From 660ea7666f10ed62e2c4aadc718bca5015ad4f93 Mon Sep 17 00:00:00 2001 From: Igor Barcik Date: Tue, 10 Dec 2024 07:46:40 +0100 Subject: [PATCH] "Enhance PLC data monitoring and error handling" Key changes include: Improved error handling and logging in PLC data reading Added detailed debug information with pprint Restructured logging format for better readability Removed redundant docstrings Fixed PLC data reading logic with proper value extraction Added support for extra properties in logger setup Code cleanup and formatting improvements --- README.md | 3 +- energy_monitor.py | 84 +++++++++++++++++++---------------------------- logger_setup.py | 1 + main.py | 37 ++++++++++----------- 4 files changed, 54 insertions(+), 71 deletions(-) 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