"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
This commit is contained in:
Igor Barcik 2024-12-10 07:46:40 +01:00
parent 7447dd8e43
commit 660ea7666f
4 changed files with 54 additions and 71 deletions

View File

@ -1,3 +1,4 @@
# IndustrialEnergyTracker
Periodically read values of air and energy consumption from Siemens PLC and PACs meters
Periodically read values of air and energy consumption from Siemens PLC and PACs meters.
Yes

View File

@ -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()

View File

@ -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

37
main.py
View File

@ -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