rattail-tempmon/rattail_tempmon/client.py

210 lines
7.7 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8; -*-
2016-12-05 19:06:34 -06:00
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
2016-12-05 19:06:34 -06:00
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
2016-12-05 19:06:34 -06:00
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
2016-12-05 19:06:34 -06:00
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
2016-12-05 19:06:34 -06:00
#
################################################################################
"""
TempMon client daemon
"""
import time
import datetime
import random
2016-12-05 19:06:34 -06:00
import socket
import logging
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm.exc import NoResultFound
2016-12-05 19:06:34 -06:00
from rattail.daemon import Daemon
from rattail_tempmon.db import Session, model as tempmon
from rattail.exceptions import ConfigurationError
2016-12-05 19:06:34 -06:00
log = logging.getLogger(__name__)
class TempmonClient(Daemon):
"""
Linux daemon implementation of Tempmon client
"""
def run(self):
"""
This method is invoked upon daemon startup. It is meant to run/loop
"forever" or until daemon stop.
"""
# maybe generate random data instead of reading from true probe
self.dummy_probes = self.config.getbool('tempmon.client', 'dummy_probes', default=False)
2016-12-05 19:06:34 -06:00
# figure out which client we are
hostname = self.config.get('tempmon.client', 'hostname', default=socket.gethostname())
log.info("i think my hostname is: %s", hostname)
2016-12-05 19:06:34 -06:00
session = Session()
try:
client = session.query(tempmon.Client)\
.filter_by(hostname=hostname)\
.one()
except NoResultFound:
session.close()
raise ConfigurationError("No tempmon client configured for hostname: {}".format(hostname))
2016-12-05 19:06:34 -06:00
client_uuid = client.uuid
self.delay = client.delay or 60
2016-12-05 19:06:34 -06:00
session.close()
# main loop: take readings, pause, repeat
self.failed_checks = 0
2016-12-05 19:06:34 -06:00
while True:
self.take_readings(client_uuid)
time.sleep(self.delay)
def take_readings(self, client_uuid):
"""
Take new readings for all enabled probes on this client.
"""
# log.debug("taking readings")
session = Session()
2016-12-05 19:06:34 -06:00
try:
client = session.get(tempmon.Client, client_uuid)
self.delay = client.delay or 60
if client.enabled:
for probe in client.enabled_probes():
self.take_reading(session, probe)
session.flush()
# one more thing, make sure our client appears "online"
if not client.online:
client.online = True
except Exception as error:
log_error = True
self.failed_checks += 1
session.rollback()
# our goal here is to suppress logging when we see connection
# errors which are due to a simple postgres restart. but if they
# keep coming then we'll go ahead and log them (sending email)
if isinstance(error, OperationalError):
# this first test works upon first DB restart, as well as the
# first time after DB stop. but in the case of DB stop,
# subsequent errors will instead match the second test
if error.connection_invalidated or (
'could not connect to server: Connection refused' in str(error)):
# only suppress logging for 3 failures, after that we let them go
# TODO: should make the max attempts configurable
if self.failed_checks < 4:
log_error = False
log.debug("database connection failure #%s: %s",
self.failed_checks,
str(error))
# send error email unless we're suppressing it for now
if log_error:
log.exception("Failed to read/record temperature data (but will keep trying)")
else: # taking readings was successful
self.failed_checks = 0
session.commit()
2016-12-05 19:06:34 -06:00
finally:
session.close()
2016-12-05 19:06:34 -06:00
def take_reading(self, session, probe):
"""
Take a single reading and add to Rattail database.
"""
reading = tempmon.Reading()
try:
reading.degrees_f = self.read_temp(probe)
except:
log.exception("Failed to read temperature (but will keep trying) for probe: %s", probe)
else:
# a reading of 185.0 °F indicates some sort of power issue. when this
# happens we log an error (which sends basic email) but do not record
# the temperature. that way the server doesn't see the 185.0 reading
# and send out a "false alarm" about the temperature being too high.
# https://www.controlbyweb.com/support/faq/temp-sensor-reading-error.html
if reading.degrees_f == 185.0:
log.error("got reading of 185.0 from probe: %s", probe.description)
else: # we have a good reading
reading.client = probe.client
reading.probe = probe
reading.taken = datetime.datetime.utcnow()
session.add(reading)
return reading
2016-12-05 19:06:34 -06:00
def read_temp(self, probe):
"""
Check for good reading, then format temperature to our liking
"""
if self.dummy_probes:
return self.random_temp(probe)
2016-12-05 19:06:34 -06:00
lines = self.read_temp_raw(probe)
while lines[0].strip()[-3:] != 'YES':
time.sleep(0.2)
lines = self.read_temp_raw(probe)
equals_pos = lines[1].find('t=')
if equals_pos != -1:
temp_string = lines[1][equals_pos+2:]
# temperature data comes in as celsius
2016-12-05 19:06:34 -06:00
temp_c = float(temp_string) / 1000.0
# convert celsius to fahrenheit
2016-12-05 19:06:34 -06:00
temp_f = temp_c * 9.0 / 5.0 + 32.0
return round(temp_f,4)
def read_temp_raw(self, probe):
"""
Function that gets the raw temp data
"""
with open(probe.device_path, 'rt') as therm_file:
return therm_file.readlines()
def random_temp(self, probe):
last_reading = probe.last_reading()
if last_reading:
volatility = 2
# try to keep somewhat of a tight pattern, so graphs look reasonable
last_degrees = float(last_reading.degrees_f)
temp = random.uniform(last_degrees - volatility * 3, last_degrees + volatility * 3)
if temp > (probe.critical_temp_max + volatility * 2):
temp -= volatility
elif temp < (probe.critical_temp_min - volatility * 2):
temp += volatility
else:
temp = random.uniform(probe.critical_temp_min - 5, probe.critical_temp_max + 5)
return round(temp, 4)
2016-12-05 19:06:34 -06:00
def make_daemon(config, pidfile=None):
"""
Returns a tempmon client daemon instance.
"""
if not pidfile:
pidfile = config.get('rattail.tempmon', 'client.pid_path',
default='/var/run/rattail/tempmon-client.pid')
return TempmonClient(pidfile, config=config)