diff --git a/.github/workflows/docker-build-develop.yml b/.github/workflows/docker-build-develop.yml new file mode 100644 index 0000000..e155661 --- /dev/null +++ b/.github/workflows/docker-build-develop.yml @@ -0,0 +1,49 @@ +name: Test and Build Python Image to ECR + +on: + pull_request: + branches: + - main + +jobs: + test-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Streamlit sanity check + run: | + streamlit --version + python -m pytest || echo "Tests not configured, skipping..." + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to ECR + env: + ECR_REGISTRY: ${{ secrets.AWS_ECR_REGISTRY }} + ECR_REPOSITORY: streamlit-app + IMAGE_TAG: streamlit-latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG diff --git a/requirements.txt b/requirements.txt index c9a0bf4..3a4eee6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,6 +47,7 @@ referencing==0.36.2 requests==2.32.3 rpds-py==0.24.0 s3transfer==0.12.0 +scipy==1.15.3 six==1.17.0 smmap==5.0.2 streamlit==1.45.0 diff --git a/service/simulation/CurrentSimulator.py b/service/simulation/CurrentSimulator.py index cd03ef0..ff63f5d 100644 --- a/service/simulation/CurrentSimulator.py +++ b/service/simulation/CurrentSimulator.py @@ -1,6 +1,6 @@ from .SimulatorInterface2 import SimulatorInterface2 from simulate_type.simulate_list import generate_current_data -import random +from scipy.stats import truncnorm class CurrentSimulator(SimulatorInterface2): def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None): @@ -20,7 +20,14 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired" self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}" self.target_current = None # 초기값 설정(shadow 용) - + self.mu = 62.51 + self.sigma = 33.76 + lower = 0 + upper = self.mu + 3 * self.sigma + self.a = (lower - self.mu) / self.sigma + self.b = (upper - self.mu) / self.sigma + + # 데이터 생성 로직 정의 def _generate_data(self) -> dict: return { @@ -28,7 +35,8 @@ def _generate_data(self) -> dict: "equipId": self.equip_id, "sensorId": self.sensor_id, "sensorType": self.type, - "val": round(random.uniform(0.1 + self.idx, 10.0 + self.idx), 2) + # "val": round(random.uniform(0.1 + self.idx, 10.0 + self.idx), 2) + "val": round(truncnorm.rvs(self.a, self.b, loc=self.mu, scale=self.sigma), 2) # 0: 7, 1: 7이상, 2: 30 이상 최소값은 0 } ################################################ diff --git a/service/simulation/DustSimulator.py b/service/simulation/DustSimulator.py index c251839..18f9fb8 100644 --- a/service/simulation/DustSimulator.py +++ b/service/simulation/DustSimulator.py @@ -1,5 +1,6 @@ from .SimulatorInterface2 import SimulatorInterface2 import random +from scipy.stats import truncnorm class DustSimulator(SimulatorInterface2): def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None): @@ -19,6 +20,14 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired" self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}" self.target_current = None # 초기값 설정(shadow 용) + + self.mu = 180 # 평균 미세먼지 수치 + self.sigma = 60 # 표준편차 + self.lower = 0 + self.upper = self.mu + 3 * self.sigma + + self.a = (self.lower - self.mu) / self.sigma + self.b = (self.upper - self.mu) / self.sigma # 데이터 생성 로직 정의 def _generate_data(self) -> dict: @@ -27,7 +36,7 @@ def _generate_data(self) -> dict: "equipId": self.equip_id, "sensorId": self.sensor_id, "sensorType": self.type, - "val": round(random.uniform(5.0 + self.idx, 50.0 + self.idx), 2) + "val": round(truncnorm.rvs(self.a, self.b, loc=self.mu, scale=self.sigma), 2) } ################################################ diff --git a/service/simulation/HumiditySimulator.py b/service/simulation/HumiditySimulator.py index bf41a59..49ad278 100644 --- a/service/simulation/HumiditySimulator.py +++ b/service/simulation/HumiditySimulator.py @@ -39,7 +39,7 @@ def _generate_data(self) -> dict: "equipId": self.equip_id, "sensorId": self.sensor_id, "sensorType": self.type, - "val": round(random.uniform(20.0 + self.idx, 80.0 + self.idx), 2) + "val": round(random.gauss(mu = 11.68, sigma = 29.38), 2) # 0: 60이하,1: 60초과, 2: 80 초과과 } ################################################ diff --git a/service/simulation/SimulatorInterface2.py b/service/simulation/SimulatorInterface2.py index 17d12b1..9ad7929 100644 --- a/service/simulation/SimulatorInterface2.py +++ b/service/simulation/SimulatorInterface2.py @@ -4,6 +4,7 @@ import threading from mqtt_util.publish import AwsMQTT import time +from scipy.stats import truncnorm class SimulatorInterface2(ABC): def __init__(self, idx: int, zone_id: str, equip_id: str, interval: int, msg_count: int, conn:AwsMQTT=None): # 센서 idx를 받기 @@ -120,4 +121,16 @@ def start_publishing(self): # self.thread.join() def stop(self): - self.stop_event.set() # 스레드 종료 이벤트 설정 \ No newline at end of file + self.stop_event.set() # 스레드 종료 이벤트 설정 + + def generate_truncated_normal(self, mu: float, sigma: float, lower: float = None, upper: float = None) -> float: + # 기본값 설정: 평균 이상의 값만 생성 + if lower is None: + lower = mu + if upper is None: + upper = mu + 3 * sigma # 거의 대부분의 값 포함 (필요시 조정) + + # truncnorm은 정규화된 구간 [a, b]를 사용하므로 변환 필요 + a, b = (lower - mu) / sigma, (upper - mu) / sigma + value = truncnorm.rvs(a, b, loc=mu, scale=sigma) + return round(value, 2) diff --git a/service/simulation/TempSimulator.py b/service/simulation/TempSimulator.py index 53cdb4a..e204eb0 100644 --- a/service/simulation/TempSimulator.py +++ b/service/simulation/TempSimulator.py @@ -1,5 +1,7 @@ from .SimulatorInterface2 import SimulatorInterface2 import random +from scipy.stats import truncnorm + class TempSimulator(SimulatorInterface2): def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None): @@ -32,6 +34,17 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co self.target_temperature = None # 초기값 설정(shadow 용) + self.mu = 25 # 평균 온도 (정상 범위: 18~21℃) + self.sigma = 10 # 표준편차 (온도의 변동폭) + + # 절단 범위 설정 (최소값 -35℃, 최대값 50℃로 설정) + self.lower = -35 + self.upper = 50 + + # 정규분포 범위의 a, b 값 계산 + self.a = (self.lower - self.mu) / self.sigma + self.b = (self.upper - self.mu) / self.sigma + ################################################z # 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현) # 예) 온도, 습도, 진동, 전류 등등 @@ -43,7 +56,7 @@ def _generate_data(self) -> dict: "equipId": self.equip_id, "sensorId": self.sensor_id, "sensorType": self.type, - "val": round(random.uniform(20.0 + self.idx, 80.0 + self.idx), 2) + "val": round(truncnorm.rvs(self.a, self.b, loc=self.mu, scale=self.sigma), 2) } ################################################ diff --git a/service/simulation/VibrationSimulator.py b/service/simulation/VibrationSimulator.py index ec274c6..06ff015 100644 --- a/service/simulation/VibrationSimulator.py +++ b/service/simulation/VibrationSimulator.py @@ -1,6 +1,7 @@ from .SimulatorInterface2 import SimulatorInterface2 from simulate_type.simulate_list import generate_vibration_data import random +from scipy.stats import truncnorm class VibrationSimulator(SimulatorInterface2): def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None): @@ -20,7 +21,17 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired" self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}" self.target_vibration = None # Initial value for shadow) - + self.mu = 3.5 # 평균 진동값 + self.sigma = 2 # 표준편차 (진동의 변동폭) + + # 절단 범위 설정 (최소값 0, 최대값 10으로 설정) + self.lower = 0 + self.upper = 10 + + # 정규분포 범위의 a, b 값 계산 + self.a = (self.lower - self.mu) / self.sigma + self.b = (self.upper - self.mu) / self.sigma + # 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현) def _generate_data(self) -> dict: return { @@ -28,7 +39,7 @@ def _generate_data(self) -> dict: "equipId": self.equip_id, "sensorId": self.sensor_id, "sensorType": self.type, - "val": round(random.uniform(0.1 + self.idx, 10.0 + self.idx), 2) + "val": round(truncnorm.rvs(self.a, self.b, loc=self.mu, scale=self.sigma), 2) } diff --git a/service/simulation/simulateTest.py b/service/simulation/simulateTest.py index 7a5780d..5bd76f9 100644 --- a/service/simulation/simulateTest.py +++ b/service/simulation/simulateTest.py @@ -81,5 +81,5 @@ def run_simulation_from_json(json_file_path): if __name__ == "__main__": # JSON 파일 경로 - json_file_path = "service/simulation/simulation_cconfig.json" + json_file_path = "simulation_cconfig.json" run_simulation_from_json(json_file_path) \ No newline at end of file