diff --git a/energy_monitor.py b/energy_monitor.py index 40ea40f..a2bc058 100644 --- a/energy_monitor.py +++ b/energy_monitor.py @@ -1,38 +1,29 @@ -import logging import os +import platform import socket from dataclasses import dataclass from datetime import datetime, timedelta from typing import List, Tuple import pyodbc -import seqlog import snap7 from dotenv import load_dotenv from snap7.util.getters import get_lreal, get_ulint load_dotenv() +# Determine the correct driver based on OS +SQL_DRIVER = "ODBC Driver 18 for SQL Server" if platform.system() == "Linux" else "SQL Server" # Configuration CONN_STR = ( - f"Driver={{SQL Server}};" + f"Driver={{{SQL_DRIVER}}};" f"Server={os.getenv('DB_SERVER')};" f"Database={os.getenv('DB_NAME')};" f"UID={os.getenv('DB_USER')};" f"PWD={os.getenv('DB_PASSWORD')};" + "TrustServerCertificate=yes;" ) -# Logger setup -root_logger = logging.getLogger() -# Logger setup -seq_logger = seqlog.log_to_seq( - server_url=os.getenv("SEQ_URL"), - api_key=os.getenv("SEQ_API_KEY"), - level=logging.INFO, - support_extra_properties=True, -) - - @dataclass class PlcConfig: """ @@ -53,10 +44,13 @@ class PlcConfig: id: int ip: str db_number: int + is_enabled: bool air_offset: int energy_offset: int - runstatus_offset: int - is_enabled: bool + location: str + last_energy_read: float + last_air_read: float + runstop_status_offset: int @dataclass @@ -79,7 +73,7 @@ class DatabaseManager: with pyodbc.connect(self.conn_str) as conn: with conn.cursor() as cursor: cursor.execute(""" - SELECT Id, Ip, DbNumber, AirDbOffset, EnergyDbOffset, RunStatusDbOffset,IsEnable + SELECT Id, Ip, DbNumber, IsEnable, AirDbOffset, EnergyDbOffset, Location, LastEnergyRead, LastAirRead, RunStopStatusDbOffset FROM sch.Plc WHERE IsEnable = 1 """) @@ -145,9 +139,9 @@ class PlcManager: try: plc.connect(config.ip, 0, 1) db_data = plc.db_read( - config.db_number, config.air_offset, config.energy_offset + 10 + config.db_number, 0, config.runstop_status_offset ) # Read up to energy offset + 8 bytes - air_value = get_ulint(db_data, config.air_offset) + 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) diff --git a/logger_setup.py b/logger_setup.py new file mode 100644 index 0000000..6822837 --- /dev/null +++ b/logger_setup.py @@ -0,0 +1,37 @@ +import logging +import os +import json +import seqlog + + +class CustomFormatter(logging.Formatter): + def format(self, record): + if hasattr(record, "extra"): + extra_formatted = json.dumps(record.extra, indent=2) + return f"{self.formatTime(record)} [{record.levelname}] {record.getMessage()}\nExtra Data: {extra_formatted}" + return f"{self.formatTime(record)} [{record.levelname}] {record.getMessage()}" + + +def setup_logger(): + logger = logging.getLogger("IndustrialEnergyTracker") + logger.setLevel(logging.DEBUG) + + # Console handler with custom formatting + console_handler = logging.StreamHandler() + console_handler.setFormatter(CustomFormatter()) + logger.addHandler(console_handler) + + # SEQ logging if configured with optimized batch settings + seq_url = os.getenv("SEQ_URL") + seq_api_key = os.getenv("SEQ_API_KEY") + + if seq_url and seq_api_key: + seqlog.log_to_seq( + server_url=seq_url, + api_key=seq_api_key, + level=logging.DEBUG, + batch_size=50, # Increased batch size + auto_flush_timeout=10, # Increased flush timeout to 10 seconds + ) + + return logger diff --git a/main.py b/main.py index 37fb56b..c127e20 100644 --- a/main.py +++ b/main.py @@ -1,21 +1,44 @@ +import os import time from datetime import datetime +import pprint -from energy_monitor import CONN_STR, DatabaseManager, PlcManager, root_logger +from energy_monitor import CONN_STR, DatabaseManager, PlcManager +from logger_setup import setup_logger # Globals DEVICE_DELAY = 1 # Delay between device data fetches POOL_RATE = 2 # 2s polling rate reading of NextRead value from database -FALLBACK_INTERVAL = 3600 # 1h interval in case db read issue +FALLBACK_INTERVAL = 20 # 1h = 3600s interval in case db read issue + + +# Logger setup +logger = setup_logger() 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={ + "plc_configs": [ + { + "id": plc.id, + "ip": plc.ip, + "location": plc.location, + "is_enabled": plc.is_enabled, + "db_number": plc.db_number, + } + for plc in plc_configs + ], + "timestamp": datetime.now().isoformat(), + }, + ) for plc_config in plc_configs: try: if not plc_manager.check_connection(plc_config.ip): - root_logger.error( + logger.error( "PLC connection failed", extra={ "PlcId": plc_config.id, @@ -28,7 +51,7 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): air_value, energy_value = plc_manager.read_plc_data(plc_config) db_manager.save_energy_data(plc_config.id, energy_value, air_value, True) - root_logger.info( + logger.info( "✅ Data successfully read and saved", extra={ "PlcId": plc_config.id, @@ -42,7 +65,7 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): time.sleep(DEVICE_DELAY) # n-second delay between devices except Exception as e: - root_logger.error( + logger.error( "Error processing PLC", extra={ "PlcId": plc_config.id, @@ -54,6 +77,23 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager): def main(): + pprint.pp( + { + "SQL_Config": { + "Server": os.getenv("DB_SERVER"), + "Database": os.getenv("DB_NAME"), + "User": os.getenv("DB_USER"), + # Masking password for security + "Password": "****", + }, + "SEQ_Config": { + "URL": os.getenv("SEQ_URL"), + # Masking API key for security + "API_Key": "****", + }, + "timestamp": datetime.now().isoformat(), + } + ) db_manager = DatabaseManager(CONN_STR) plc_manager = PlcManager() @@ -68,7 +108,13 @@ def main(): time.sleep(POOL_RATE) except Exception as e: - root_logger.error( + pprint.pp( + { + "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()}, ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..21b3def --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +# requirements.txt + +certifi==2024.8.30 +charset-normalizer==3.4.0 +idna==3.10 +pyodbc==5.2.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-snap7==2.0.2 +PyYAML==6.0.2 +requests==2.32.3 +seqlog==0.3.31 +six==1.16.0 +urllib3==2.2.3