"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:
parent
7447dd8e43
commit
660ea7666f
@ -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
|
@ -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()
|
@ -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
37
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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user