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:
parent
fc3aeb9a14
commit
7447dd8e43
@ -1,38 +1,29 @@
|
|||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import socket
|
import socket
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import pyodbc
|
import pyodbc
|
||||||
import seqlog
|
|
||||||
import snap7
|
import snap7
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from snap7.util.getters import get_lreal, get_ulint
|
from snap7.util.getters import get_lreal, get_ulint
|
||||||
|
|
||||||
load_dotenv()
|
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
|
# Configuration
|
||||||
CONN_STR = (
|
CONN_STR = (
|
||||||
f"Driver={{SQL Server}};"
|
f"Driver={{{SQL_DRIVER}}};"
|
||||||
f"Server={os.getenv('DB_SERVER')};"
|
f"Server={os.getenv('DB_SERVER')};"
|
||||||
f"Database={os.getenv('DB_NAME')};"
|
f"Database={os.getenv('DB_NAME')};"
|
||||||
f"UID={os.getenv('DB_USER')};"
|
f"UID={os.getenv('DB_USER')};"
|
||||||
f"PWD={os.getenv('DB_PASSWORD')};"
|
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
|
@dataclass
|
||||||
class PlcConfig:
|
class PlcConfig:
|
||||||
"""
|
"""
|
||||||
@ -53,10 +44,13 @@ class PlcConfig:
|
|||||||
id: int
|
id: int
|
||||||
ip: str
|
ip: str
|
||||||
db_number: int
|
db_number: int
|
||||||
|
is_enabled: bool
|
||||||
air_offset: int
|
air_offset: int
|
||||||
energy_offset: int
|
energy_offset: int
|
||||||
runstatus_offset: int
|
location: str
|
||||||
is_enabled: bool
|
last_energy_read: float
|
||||||
|
last_air_read: float
|
||||||
|
runstop_status_offset: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -79,7 +73,7 @@ class DatabaseManager:
|
|||||||
with pyodbc.connect(self.conn_str) as conn:
|
with pyodbc.connect(self.conn_str) as conn:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT Id, Ip, DbNumber, AirDbOffset, EnergyDbOffset, RunStatusDbOffset,IsEnable
|
SELECT Id, Ip, DbNumber, IsEnable, AirDbOffset, EnergyDbOffset, Location, LastEnergyRead, LastAirRead, RunStopStatusDbOffset
|
||||||
FROM sch.Plc
|
FROM sch.Plc
|
||||||
WHERE IsEnable = 1
|
WHERE IsEnable = 1
|
||||||
""")
|
""")
|
||||||
@ -145,9 +139,9 @@ class PlcManager:
|
|||||||
try:
|
try:
|
||||||
plc.connect(config.ip, 0, 1)
|
plc.connect(config.ip, 0, 1)
|
||||||
db_data = plc.db_read(
|
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
|
) # 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
|
# TODO if (air_value < prev_air_val): update in plc
|
||||||
energy_value = get_lreal(db_data, config.energy_offset)
|
energy_value = get_lreal(db_data, config.energy_offset)
|
||||||
stauts_value = getattr(db_data, config.runstatus_offset)
|
stauts_value = getattr(db_data, config.runstatus_offset)
|
||||||
|
37
logger_setup.py
Normal file
37
logger_setup.py
Normal 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
60
main.py
@ -1,21 +1,44 @@
|
|||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
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
|
# Globals
|
||||||
DEVICE_DELAY = 1 # Delay between device data fetches
|
DEVICE_DELAY = 1 # Delay between device data fetches
|
||||||
POOL_RATE = 2 # 2s polling rate reading of NextRead value from database
|
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):
|
def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager):
|
||||||
plc_configs = db_manager.get_plc_configs()
|
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:
|
for plc_config in plc_configs:
|
||||||
try:
|
try:
|
||||||
if not plc_manager.check_connection(plc_config.ip):
|
if not plc_manager.check_connection(plc_config.ip):
|
||||||
root_logger.error(
|
logger.error(
|
||||||
"PLC connection failed",
|
"PLC connection failed",
|
||||||
extra={
|
extra={
|
||||||
"PlcId": plc_config.id,
|
"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)
|
air_value, energy_value = plc_manager.read_plc_data(plc_config)
|
||||||
db_manager.save_energy_data(plc_config.id, energy_value, air_value, True)
|
db_manager.save_energy_data(plc_config.id, energy_value, air_value, True)
|
||||||
|
|
||||||
root_logger.info(
|
logger.info(
|
||||||
"✅ Data successfully read and saved",
|
"✅ Data successfully read and saved",
|
||||||
extra={
|
extra={
|
||||||
"PlcId": plc_config.id,
|
"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
|
time.sleep(DEVICE_DELAY) # n-second delay between devices
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
root_logger.error(
|
logger.error(
|
||||||
"Error processing PLC",
|
"Error processing PLC",
|
||||||
extra={
|
extra={
|
||||||
"PlcId": plc_config.id,
|
"PlcId": plc_config.id,
|
||||||
@ -54,6 +77,23 @@ def process_plc_devices(db_manager: DatabaseManager, plc_manager: PlcManager):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
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)
|
db_manager = DatabaseManager(CONN_STR)
|
||||||
plc_manager = PlcManager()
|
plc_manager = PlcManager()
|
||||||
|
|
||||||
@ -68,7 +108,13 @@ def main():
|
|||||||
time.sleep(POOL_RATE)
|
time.sleep(POOL_RATE)
|
||||||
|
|
||||||
except Exception as e:
|
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.",
|
"Main loop error! Falling back to 1h interval.",
|
||||||
extra={"error": str(e), "timestamp": datetime.now().isoformat()},
|
extra={"error": str(e), "timestamp": datetime.now().isoformat()},
|
||||||
)
|
)
|
||||||
|
14
requirements.txt
Normal file
14
requirements.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user