diff --git a/README.md b/README.md index 9875cbc..ab7a363 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # IndustrialEnergyTracker -Periodically read values of air and energy consumption from Siemens PLC and PACs meters \ No newline at end of file +Periodically read values of air and energy consumption from Siemens PLC and PACs meters. +Yes \ No newline at end of file diff --git a/energy_monitor.py b/energy_monitor.py index a2bc058..e766455 100644 --- a/energy_monitor.py +++ b/energy_monitor.py @@ -1,5 +1,6 @@ import os import platform +import pprint import socket from dataclasses import dataclass from datetime import datetime, timedelta @@ -8,12 +9,12 @@ from typing import List, Tuple import pyodbc import snap7 from dotenv import load_dotenv -from snap7.util.getters import get_lreal, get_ulint - +from snap7.util.getters import get_lreal, get_bool load_dotenv() - # Determine the correct driver based on OS -SQL_DRIVER = "ODBC Driver 18 for SQL Server" if platform.system() == "Linux" else "SQL Server" +SQL_DRIVER = ( + "ODBC Driver 18 for SQL Server" if platform.system() == "Linux" else "SQL Server" +) # Configuration CONN_STR = ( f"Driver={{{SQL_DRIVER}}};" @@ -26,21 +27,6 @@ CONN_STR = ( @dataclass class PlcConfig: - """ - Represents the configuration for a Programmable Logic Controller (PLC) device. - - This class holds the necessary information to connect to and read data from a PLC, - including its IP address, database number, and offsets for air and energy data. - - Attributes: - `id` (int): The unique identifier for the PLC. - `ip` (str): The IP address of the PLC. - `db_number` (int): The database number to read from the PLC. - `air_offset` (int): The offset within the database for the air data. - `energy_offset` (int): The offset within the database for the energy data. - `is_enabled` (bool): Whether the PLC is enabled and should be monitored. - """ - id: int ip: str db_number: int @@ -52,24 +38,15 @@ class PlcConfig: last_air_read: float runstop_status_offset: int - @dataclass class SchedulerConfig: interval: int next_read: datetime - class DatabaseManager: def __init__(self, connection_string: str): self.conn_str = connection_string - def get_plc_configs(self) -> List[PlcConfig]: - """ - Retrieves a list of enabled PLC configurations from the database. - - Returns: - List[PlcConfig]: A list of PlcConfig objects representing the enabled PLCs. - """ with pyodbc.connect(self.conn_str) as conn: with conn.cursor() as cursor: cursor.execute(""" @@ -78,14 +55,7 @@ class DatabaseManager: WHERE IsEnable = 1 """) return [PlcConfig(*row) for row in cursor.fetchall()] - def get_scheduler_config(self) -> SchedulerConfig: - """ - Retrieves the scheduler configuration from the database. - - Returns: - SchedulerConfig: The scheduler configuration, including the interval and the next read time. - """ with pyodbc.connect(self.conn_str) as conn: with conn.cursor() as cursor: cursor.execute(""" @@ -99,7 +69,6 @@ class DatabaseManager: if row else SchedulerConfig(30, datetime.now()) ) - def save_energy_data(self, plc_id: int, energy: float, air: float, state: bool): with pyodbc.connect(self.conn_str) as conn: with conn.cursor() as cursor: @@ -111,7 +80,6 @@ class DatabaseManager: (energy, plc_id, air, state, datetime.now()), ) conn.commit() - def update_next_read(self, interval_seconds: int): next_read = datetime.now() + timedelta(seconds=interval_seconds) with pyodbc.connect(self.conn_str) as conn: @@ -126,25 +94,41 @@ class DatabaseManager: ) conn.commit() - class PlcManager: - @staticmethod - def check_connection(ip: str, port: int = 102, timeout: int = 1) -> bool: + def check_connection(self, ip: str, port: int = 102, timeout: int = 1) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(timeout) return sock.connect_ex((ip, port)) == 0 - def read_plc_data(self, config: PlcConfig) -> Tuple[float, float, bool]: plc = snap7.client.Client() try: plc.connect(config.ip, 0, 1) - db_data = plc.db_read( - config.db_number, 0, config.runstop_status_offset - ) # Read up to energy offset + 8 bytes - air_value = get_lreal(db_data, config.air_offset) - # TODO if (air_value < prev_air_val): update in plc - energy_value = get_lreal(db_data, config.energy_offset) - stauts_value = getattr(db_data, config.runstatus_offset) - return air_value, energy_value, stauts_value + data = plc.db_read(config.db_number, 0, config.runstop_status_offset) # Define range of bytes to read + energy_value = get_lreal(data, config.energy_offset) # Read energy value + air_value = get_lreal(data, config.air_offset) # Read air value + # run_status_value = get_bool(data, config.runstop_status_offset, 1) # Read run start value + return air_value, energy_value, True + except Exception as e: + pprint.pp( + { + "msg": "Failed to read PLC data", + "plc_config": { + "id": config.id, + "ip": config.ip, + "db_number": config.db_number, + "air_offset": config.air_offset, + "energy_offset": config.energy_offset, + "runstop_status_offset": config.runstop_status_offset, + "location": config.location, + "ene_val": energy_value, + "air_val": air_value, + "run_status_val": run_status_value, + "timestamp": datetime.now().isoformat() + }, + "error": str(e), + "timestamp": datetime.now().isoformat() + } + ) + raise finally: - plc.disconnect() + plc.disconnect() \ No newline at end of file diff --git a/logger_setup.py b/logger_setup.py index 6822837..4b0b519 100644 --- a/logger_setup.py +++ b/logger_setup.py @@ -32,6 +32,7 @@ def setup_logger(): level=logging.DEBUG, batch_size=50, # Increased batch size auto_flush_timeout=10, # Increased flush timeout to 10 seconds + support_extra_properties=True, ) return logger diff --git a/main.py b/main.py index c127e20..9d5379b 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ import os +import pprint import time from datetime import datetime -import pprint from energy_monitor import CONN_STR, DatabaseManager, PlcManager from logger_setup import setup_logger @@ -11,7 +11,6 @@ DEVICE_DELAY = 1 # Delay between device data fetches POOL_RATE = 2 # 2s polling rate reading of NextRead value from database FALLBACK_INTERVAL = 20 # 1h = 3600s interval in case db read issue - # Logger setup logger = setup_logger() @@ -20,8 +19,8 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): plc_configs = db_manager.get_plc_configs() # If debug print out plc_configs to logs logger.debug( - "Retrieved PLC configurations", - extra={ + { + "msg": "Retrieved PLC configurations", "plc_configs": [ { "id": plc.id, @@ -39,21 +38,21 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): try: if not plc_manager.check_connection(plc_config.ip): logger.error( - "PLC connection failed", - extra={ + { + "msg": "PLC connection failed", "PlcId": plc_config.id, "PlcIp": plc_config.ip, "timestamp": datetime.now().isoformat(), - }, + } ) continue - air_value, energy_value = plc_manager.read_plc_data(plc_config) + air_value, energy_value, run_status = plc_manager.read_plc_data(plc_config) db_manager.save_energy_data(plc_config.id, energy_value, air_value, True) logger.info( - "✅ Data successfully read and saved", - extra={ + { + "msg": "✅ Data successfully read and saved", "PlcId": plc_config.id, "PlcIp": plc_config.ip, "energy_value": energy_value, @@ -66,8 +65,8 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): except Exception as e: logger.error( - "Error processing PLC", - extra={ + { + "msg": "Error reading PLC data", "PlcId": plc_config.id, "PlcIp": plc_config.ip, "error": str(e), @@ -77,6 +76,9 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): def main(): + db_manager = DatabaseManager(CONN_STR) + plc_manager = PlcManager() + pprint.pp( { "SQL_Config": { @@ -94,8 +96,6 @@ def main(): "timestamp": datetime.now().isoformat(), } ) - db_manager = DatabaseManager(CONN_STR) - plc_manager = PlcManager() while True: try: @@ -108,15 +108,12 @@ def main(): time.sleep(POOL_RATE) except Exception as e: - pprint.pp( + logger.error( { + "msg": "Main loop error! Falling back to 1h interval.", "error": str(e), "timestamp": datetime.now().isoformat(), - } - ) - logger.error( - "Main loop error! Falling back to 1h interval.", - extra={"error": str(e), "timestamp": datetime.now().isoformat()}, + }, ) time.sleep(FALLBACK_INTERVAL) # Default fallback interval