Rewrite logging function to log to seq and console; Add function to change SQL driver depends on os; Add requirements.exe

This commit is contained in:
Igor Barcik 2024-11-21 14:47:49 +01:00
parent fc3aeb9a14
commit 7447dd8e43
Signed by: biggy
GPG Key ID: EA4CE0D1E2A6DC98
4 changed files with 117 additions and 26 deletions

View File

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

37
logger_setup.py Normal file
View File

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

60
main.py
View File

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

14
requirements.txt Normal file
View File

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