diff --git a/.gitignore b/.gitignore index 8b9f408..b2c6123 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ -*~ -*.pyc -dist/ rattail_tempmon.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a249452..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,53 +0,0 @@ - -# Changelog -All notable changes to rattail-tempmon will be documented in this file. - -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - -## v0.4.6 (2024-08-19) - -### Fix - -- avoid deprecated base class for config extension - -## v0.4.5 (2024-07-02) - -### Fix - -- fix signature for calls to `get_engines()` - -## v0.4.4 (2024-07-02) - -### Fix - -- avoid deprecated function for engine config - -## v0.4.3 (2024-07-01) - -### Fix - -- remove references, dependency for `six` package - -## v0.4.2 (2024-07-01) - -### Fix - -- remove legacy command definitions - -## v0.4.1 (2024-06-14) - -### Fix - -- fallback to `importlib_metadata` on older python - -## v0.4.0 (2024-06-10) - -### Feat - -- switch from setup.cfg to pyproject.toml + hatchling - - -## Older Releases - -Please see `docs/OLDCHANGES.rst` for older release notes. diff --git a/docs/OLDCHANGES.rst b/CHANGES.rst similarity index 66% rename from docs/OLDCHANGES.rst rename to CHANGES.rst index 8727078..99b430e 100644 --- a/docs/OLDCHANGES.rst +++ b/CHANGES.rst @@ -2,90 +2,6 @@ CHANGELOG ========= -NB. this file contains "old" release notes only. for newer releases -see the `CHANGELOG.md` file in the source root folder. - - -0.3.0 (2024-05-30) ------------------- - -* Migrate all commands to use ``typer``. - - -0.2.10 (2023-11-30) -------------------- - -* Update subcommand entry point group names, per wuttjamaican. - - -0.2.9 (2023-05-16) ------------------- - -* Replace ``setup.py`` contents with ``setup.cfg``. - - -0.2.8 (2023-02-12) ------------------- - -* Refactor ``Query.get()`` => ``Session.get()`` per SQLAlchemy 1.4. - - -0.2.7 (2023-02-10) ------------------- - -* Officially drop support for python2. - -* Address a warning from SQLAlchemy for ``declarative_base``. - - -0.2.6 (2022-08-06) ------------------- - -* Register email profiles provided by this pkg. - - -0.2.5 (2020-09-22) ------------------- - -* Remove config for deprecated 'tempmon_critical_temp' email. - -* Declare sort order for ``Appliance.probes`` relationship. - - -0.2.4 (2019-04-23) ------------------- - -* Make sure we use zero as fallback/default timeout values. - - -0.2.3 (2019-01-28) ------------------- - -* Add more template context for email previews. - -* Convert ``enabled`` for Client, Probe to use datetime instead of boolean. - -* Modify tempmon server logic to take "unfair" time windows into account. - - -0.2.2 (2018-10-25) ------------------- - -* Fix bug when sending certain emails while checking probe readings. - - -0.2.1 (2018-10-24) ------------------- - -* Make dummy probe use tighter pattern for random readings. - -* Add "default" probe timeout logic for server readings check. - -* Don't mark client as online unless it's also enabled. - -* Add try/catch for client's "read temp" logic. - - 0.2.0 (2018-10-19) ------------------ diff --git a/README.md b/README.rst similarity index 64% rename from README.md rename to README.rst index 979c915..39651c1 100644 --- a/README.md +++ b/README.rst @@ -1,5 +1,6 @@ -# rattail-tempmon +rattail-tempmon +=============== Rattail is a retail software framework, released under the GNU General Public License. @@ -7,5 +8,6 @@ License. This is the ``rattail-tempmon`` package, which provides a database schema, and client/server daemons for recording and processing temperature data. -Please see Rattail's [home page](https://rattailproject.org/) for more -information. +Please see Rattail's `home page`_ for more information. + +.. _home page: https://rattailproject.org/ diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 0e694fa..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,52 +0,0 @@ - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - - -[project] -name = "rattail-tempmon" -version = "0.4.6" -description = "Retail Software Framework - Temperature monitoring add-on" -readme = "README.md" -authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] -license = {text = "GNU GPL v3+"} -classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Topic :: Office/Business", - "Topic :: Software Development :: Libraries :: Python Modules", -] -dependencies = [ - "rattail[db]", - "sqlsoup", -] - - -[project.urls] -Homepage = "https://rattailproject.org" -Repository = "https://forgejo.wuttaproject.org/rattail/rattail-tempmon" -Changelog = "https://forgejo.wuttaproject.org/rattail/rattail-tempmon/src/branch/master/CHANGELOG.md" - - -[project.entry-points."rattail.config.extensions"] -tempmon = "rattail_tempmon.config:TempmonConfigExtension" - - -[project.entry-points."rattail.typer_imports"] -rattail_tempmon = "rattail_tempmon.commands" - - -[project.entry-points."rattail.emails"] -rattail_tempmon = "rattail_tempmon.emails" - - -[tool.commitizen] -version_provider = "pep621" -tag_format = "v$version" -update_changelog_on_bump = true diff --git a/rattail_tempmon/_version.py b/rattail_tempmon/_version.py index 7135faa..08b390b 100644 --- a/rattail_tempmon/_version.py +++ b/rattail_tempmon/_version.py @@ -1,9 +1,3 @@ # -*- coding: utf-8; -*- -try: - from importlib.metadata import version -except ImportError: - from importlib_metadata import version - - -__version__ = version('rattail-tempmon') +__version__ = '0.2.0' diff --git a/rattail_tempmon/client.py b/rattail_tempmon/client.py index 5173c67..2b154cb 100644 --- a/rattail_tempmon/client.py +++ b/rattail_tempmon/client.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2018 Lance Edgar # # This file is part of Rattail. # @@ -24,12 +24,15 @@ TempMon client daemon """ +from __future__ import unicode_literals, absolute_import + import time import datetime import random import socket import logging +import six from sqlalchemy.exc import OperationalError from sqlalchemy.orm.exc import NoResultFound @@ -83,16 +86,16 @@ class TempmonClient(Daemon): session = Session() try: - client = session.get(tempmon.Client, client_uuid) + client = session.query(tempmon.Client).get(client_uuid) self.delay = client.delay or 60 if client.enabled: for probe in client.enabled_probes(): self.take_reading(session, probe) - session.flush() + session.flush() - # one more thing, make sure our client appears "online" - if not client.online: - client.online = True + # one more thing, make sure our client appears "online" + if not client.online: + client.online = True except Exception as error: log_error = True @@ -108,7 +111,7 @@ class TempmonClient(Daemon): # 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)): + 'could not connect to server: Connection refused' in six.text_type(error)): # only suppress logging for 3 failures, after that we let them go # TODO: should make the max attempts configurable @@ -116,7 +119,7 @@ class TempmonClient(Daemon): log_error = False log.debug("database connection failure #%s: %s", self.failed_checks, - str(error)) + six.text_type(error)) # send error email unless we're suppressing it for now if log_error: @@ -134,28 +137,22 @@ class TempmonClient(Daemon): Take a single reading and add to Rattail database. """ reading = tempmon.Reading() + reading.degrees_f = self.read_temp(probe) - try: - reading.degrees_f = self.read_temp(probe) + # 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) - 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 + else: # we have a good reading + reading.client = probe.client + reading.probe = probe + reading.taken = datetime.datetime.utcnow() + session.add(reading) + return reading def read_temp(self, probe): """ @@ -184,18 +181,7 @@ class TempmonClient(Daemon): 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) + temp = random.uniform(probe.critical_temp_min - 5, probe.critical_temp_max + 5) return round(temp, 4) diff --git a/rattail_tempmon/commands.py b/rattail_tempmon/commands.py index 29291f1..312c4d3 100644 --- a/rattail_tempmon/commands.py +++ b/rattail_tempmon/commands.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2017 Lance Edgar # # This file is part of Rattail. # @@ -24,157 +24,137 @@ Tempmon commands """ +from __future__ import unicode_literals, absolute_import + import datetime import logging -from enum import Enum -from pathlib import Path -import typer -from typing_extensions import Annotated - -from rattail.commands import rattail_typer -from rattail.commands.typer import importer_command, typer_get_runas_user -from rattail.commands.importing import ImportCommandHandler +from rattail import commands +from rattail.time import localtime, make_utc log = logging.getLogger(__name__) -class ServiceAction(str, Enum): - start = 'start' - stop = 'stop' - - -@rattail_typer.command() -@importer_command -def export_hotcooler( - ctx: typer.Context, - **kwargs -): +class ExportHotCooler(commands.ImportSubcommand): """ Export data from Rattail-Tempmon to HotCooler """ - config = ctx.parent.rattail_config - progress = ctx.parent.rattail_progress - handler = ImportCommandHandler( - config, - import_handler_spec='rattail_tempmon.hotcooler.importing.tempmon:FromTempmonToHotCooler') - kwargs['user'] = typer_get_runas_user(ctx) - handler.run(kwargs, progress=progress) + name = 'export-hotcooler' + description = __doc__.strip() + handler_spec = 'rattail_tempmon.hotcooler.importing.tempmon:FromTempmonToHotCooler' -@rattail_typer.command() -def purge_tempmon( - ctx: typer.Context, - keep_days: Annotated[ - int, - typer.Option('--keep', - help="Number of days for which data should be kept.")] = ..., - dry_run: Annotated[ - bool, - typer.Option('--dry-run', - help="Go through the full motions and allow logging etc. to " - "occur, but rollback (abort) the transaction at the end.")] = False, -): +class PurgeTempmon(commands.Subcommand): """ Purge stale data from Tempmon database """ - config = ctx.parent.rattail_config - progress = ctx.parent.rattail_progress - do_purge(config, keep_days, dry_run=dry_run, progress=progress) + name = 'purge-tempmon' + description = __doc__.strip() + + def add_parser_args(self, parser): + parser.add_argument('--keep', metavar='DAYS', required=True, type=int, + help="Number of days for which data should be kept.") + parser.add_argument('--dry-run', action='store_true', + help="Go through the full motions and allow logging etc. to " + "occur, but rollback (abort) the transaction at the end.") + + def run(self, args): + from rattail_tempmon.db import Session as TempmonSession, model as tempmon + + cutoff = localtime(self.config).date() - datetime.timedelta(days=args.keep) + cutoff = localtime(self.config, datetime.datetime.combine(cutoff, datetime.time(0))) + session = TempmonSession() + + readings = session.query(tempmon.Reading)\ + .filter(tempmon.Reading.taken < make_utc(cutoff)) + count = readings.count() + + def purge(reading, i): + session.delete(reading) + if i % 200 == 0: + session.flush() + + self.progress_loop(purge, readings, count=count, message="Purging stale readings") + log.info("deleted {} stale readings".format(count)) + + if args.dry_run: + session.rollback() + log.info("dry run, so transaction was rolled back") + else: + session.commit() + log.info("transaction was committed") + session.close() -@rattail_typer.command() -def tempmon_client( - ctx: typer.Context, - action: Annotated[ - ServiceAction, - typer.Argument(help="Action to perform for the service.")] = ..., - pidfile: Annotated[ - Path, - typer.Option('--pidfile', '-p', - help="Path to PID file.")] = None, - # TODO: deprecate / remove this - daemonize: Annotated[ - bool, - typer.Option('--daemonize', - help="Daemonize when starting.")] = False, -): +class TempmonClient(commands.Subcommand): """ Manage the tempmon-client daemon """ - from rattail_tempmon.client import make_daemon + name = 'tempmon-client' + description = __doc__.strip() - config = ctx.parent.rattail_config - daemon = make_daemon(config, pidfile) - if action == 'start': - daemon.start(daemonize) - elif action == 'stop': - daemon.stop() + def add_parser_args(self, parser): + subparsers = parser.add_subparsers(title='subcommands') + + start = subparsers.add_parser('start', help="Start daemon") + start.set_defaults(subcommand='start') + stop = subparsers.add_parser('stop', help="Stop daemon") + stop.set_defaults(subcommand='stop') + + parser.add_argument('-p', '--pidfile', + help="Path to PID file.", metavar='PATH') + parser.add_argument('-D', '--daemonize', action='store_true', + help="Daemonize when starting.") + + def run(self, args): + from rattail_tempmon.client import make_daemon + + daemon = make_daemon(self.config, args.pidfile) + if args.subcommand == 'start': + daemon.start(args.daemonize) + elif args.subcommand == 'stop': + daemon.stop() -@rattail_typer.command() -def tempmon_problems( - ctx: typer.Context, -): - """ - Email report(s) of various Tempmon data problems - """ - from rattail_tempmon import problems - - config = ctx.parent.rattail_config - progress = ctx.parent.rattail_progress - problems.disabled_probes(config, progress=progress) - - -@rattail_typer.command() -def tempmon_server( - ctx: typer.Context, - action: Annotated[ - ServiceAction, - typer.Argument(help="Action to perform for the service.")] = ..., - pidfile: Annotated[ - Path, - typer.Option('--pidfile', '-p', - help="Path to PID file.")] = None, - # TODO: deprecate / remove this - daemonize: Annotated[ - bool, - typer.Option('--daemonize', - help="Daemonize when starting.")] = False, -): +class TempmonServer(commands.Subcommand): """ Manage the tempmon-server daemon """ - from rattail_tempmon.server import make_daemon + name = 'tempmon-server' + description = __doc__.strip() - config = ctx.parent.rattail_config - daemon = make_daemon(config, pidfile) - if action == 'start': - daemon.start(daemonize) - elif action == 'stop': - daemon.stop() + def add_parser_args(self, parser): + subparsers = parser.add_subparsers(title='subcommands') + + start = subparsers.add_parser('start', help="Start daemon") + start.set_defaults(subcommand='start') + stop = subparsers.add_parser('stop', help="Stop daemon") + stop.set_defaults(subcommand='stop') + + parser.add_argument('-p', '--pidfile', + help="Path to PID file.", metavar='PATH') + parser.add_argument('-D', '--daemonize', action='store_true', + help="Daemonize when starting.") + + def run(self, args): + from rattail_tempmon.server import make_daemon + + daemon = make_daemon(self.config, args.pidfile) + if args.subcommand == 'start': + daemon.start(args.daemonize) + elif args.subcommand == 'stop': + daemon.stop() -def do_purge(config, keep_days, dry_run=False, progress=None): - from rattail_tempmon.db import Session, model - from rattail.db.util import finalize_session +class TempmonProblems(commands.Subcommand): + """ + Email report(s) of various Tempmon data problems + """ + name = 'tempmon-problems' + description = __doc__.strip() - app = config.get_app() - cutoff = app.today() - datetime.timedelta(days=keep_days) - cutoff = app.localtime(datetime.datetime.combine(cutoff, datetime.time(0))) - session = Session() + def run(self, args): + from rattail_tempmon import problems - readings = session.query(model.Reading)\ - .filter(model.Reading.taken < app.make_utc(cutoff))\ - .all() - - def purge(reading, i): - session.delete(reading) - if i % 200 == 0: - session.flush() - - app.progress_loop(purge, readings, progress, - message="Purging stale readings") - log.info("deleted %s stale readings", len(readings)) - finalize_session(session, dry_run=dry_run) + problems.disabled_probes(self.config, progress=self.progress) diff --git a/rattail_tempmon/config.py b/rattail_tempmon/config.py index a5f1333..1c30d30 100644 --- a/rattail_tempmon/config.py +++ b/rattail_tempmon/config.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8; -*- +# -*- coding: utf-8 -*- ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2017 Lance Edgar # # This file is part of Rattail. # @@ -24,13 +24,14 @@ Tempmon config extension """ -from wuttjamaican.db import get_engines -from wuttjamaican.conf import WuttaConfigExtension +from __future__ import unicode_literals, absolute_import +from rattail.config import ConfigExtension +from rattail.db.config import get_engines from rattail_tempmon.db import Session -class TempmonConfigExtension(WuttaConfigExtension): +class TempmonConfigExtension(ConfigExtension): """ Config extension for tempmon; adds tempmon DB engine/Session etc. Expects something like this in your config: @@ -52,10 +53,10 @@ class TempmonConfigExtension(WuttaConfigExtension): def configure(self, config): # tempmon - config.tempmon_engines = get_engines(config, 'rattail_tempmon.db') + config.tempmon_engines = get_engines(config, section='rattail_tempmon.db') config.tempmon_engine = config.tempmon_engines.get('default') Session.configure(bind=config.tempmon_engine) # hotcooler - config.hotcooler_engines = get_engines(config, 'hotcooler.db') + config.hotcooler_engines = get_engines(config, section='hotcooler.db') config.hotcooler_engine = config.hotcooler_engines.get('default') diff --git a/rattail_tempmon/db/alembic/versions/fd1df160539a_make_enabled_datetime.py b/rattail_tempmon/db/alembic/versions/fd1df160539a_make_enabled_datetime.py deleted file mode 100644 index 1a327f9..0000000 --- a/rattail_tempmon/db/alembic/versions/fd1df160539a_make_enabled_datetime.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8; -*- -"""make enabled datetime - -Revision ID: fd1df160539a -Revises: a2676d3dfc1e -Create Date: 2019-01-25 18:41:01.652823 - -""" - -from __future__ import unicode_literals, absolute_import - -# revision identifiers, used by Alembic. -revision = 'fd1df160539a' -down_revision = 'a2676d3dfc1e' -branch_labels = None -depends_on = None - -import datetime -from alembic import op -import sqlalchemy as sa -import rattail.db.types - - - -def upgrade(): - - now = datetime.datetime.utcnow() - - # client - op.add_column('client', sa.Column('new_enabled', sa.DateTime(), nullable=True)) - client = sa.sql.table('client', - sa.sql.column('enabled'), - sa.sql.column('new_enabled')) - op.execute(client.update()\ - .where(client.c.enabled == True)\ - .values({'new_enabled': now})) - op.drop_column('client', 'enabled') - op.alter_column('client', 'new_enabled', new_column_name='enabled') - - # probe - op.add_column('probe', sa.Column('new_enabled', sa.DateTime(), nullable=True)) - probe = sa.sql.table('probe', - sa.sql.column('enabled'), - sa.sql.column('new_enabled')) - op.execute(probe.update()\ - .where(probe.c.enabled == True)\ - .values({'new_enabled': now})) - op.drop_column('probe', 'enabled') - op.alter_column('probe', 'new_enabled', new_column_name='enabled') - - -def downgrade(): - - # probe - op.add_column('probe', sa.Column('old_enabled', sa.Boolean(), nullable=True)) - probe = sa.sql.table('probe', - sa.sql.column('enabled'), - sa.sql.column('old_enabled')) - op.execute(probe.update()\ - .where(probe.c.enabled != None)\ - .values({'old_enabled': True})) - op.execute(probe.update()\ - .where(probe.c.enabled == None)\ - .values({'old_enabled': False})) - op.drop_column('probe', 'enabled') - op.alter_column('probe', 'old_enabled', new_column_name='enabled', nullable=False) - - # client - op.add_column('client', sa.Column('old_enabled', sa.Boolean(), nullable=True)) - client = sa.sql.table('client', - sa.sql.column('enabled'), - sa.sql.column('old_enabled')) - op.execute(client.update()\ - .where(client.c.enabled != None)\ - .values({'old_enabled': True})) - op.execute(client.update()\ - .where(client.c.enabled == None)\ - .values({'old_enabled': False})) - op.drop_column('client', 'enabled') - op.alter_column('client', 'old_enabled', new_column_name='enabled', nullable=False) diff --git a/rattail_tempmon/db/model.py b/rattail_tempmon/db/model.py index 6fda2aa..4abbd29 100644 --- a/rattail_tempmon/db/model.py +++ b/rattail_tempmon/db/model.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2023 Lance Edgar +# Copyright © 2010-2018 Lance Edgar # # This file is part of Rattail. # @@ -24,14 +24,14 @@ Data models for tempmon """ +from __future__ import unicode_literals, absolute_import + import datetime +import six import sqlalchemy as sa from sqlalchemy import orm -try: - from sqlalchemy.orm import declarative_base -except ImportError: - from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.ext.declarative import declarative_base from rattail import enum from rattail.db.model import uuid_column @@ -41,6 +41,7 @@ from rattail.db.model.core import ModelBase Base = declarative_base(cls=ModelBase) +@six.python_2_unicode_compatible class Appliance(Base): """ Represents an appliance which is monitored by tempmon. @@ -80,6 +81,7 @@ class Appliance(Base): return self.name +@six.python_2_unicode_compatible class Client(Base): """ Represents a tempmon client. @@ -109,11 +111,11 @@ class Client(Base): Any arbitrary notes for the client. """) - enabled = sa.Column(sa.DateTime(), nullable=True, doc=""" - This will either be the date/time when the client was most recently - enabled, or null if it is not currently enabled. If set, the client will - be expected to take readings (but only for "enabled" probes) and the server - will monitor them to ensure they are within the expected range etc. + enabled = sa.Column(sa.Boolean(), nullable=False, default=False, doc=""" + Whether the client should be considered enabled (active). If set, the + client will be expected to take readings (but only for "enabled" probes) + and the server will monitor them to ensure they are within the expected + range etc. """) online = sa.Column(sa.Boolean(), nullable=False, default=False, doc=""" @@ -137,6 +139,7 @@ class Client(Base): return [probe for probe in self.probes if probe.enabled] +@six.python_2_unicode_compatible class Probe(Base): """ Represents a probe connected to a tempmon client. @@ -175,7 +178,6 @@ class Probe(Base): """, backref=orm.backref( 'probes', - order_by='Probe.description', doc=""" List of probes which monitor this appliance. """)) @@ -189,13 +191,7 @@ class Probe(Base): """) device_path = sa.Column(sa.String(length=255), nullable=True) - - enabled = sa.Column(sa.DateTime(), nullable=True, doc=""" - This will either be the date/time when the probe was most recently enabled, - or null if it is not currently enabled. If set, the client will be - expected to take readings for this probe, and the server will monitor them - to ensure they are within the expected range etc. - """) + enabled = sa.Column(sa.Boolean(), nullable=False, default=True) critical_temp_max = sa.Column(sa.Integer(), nullable=False, doc=""" Maximum high temperature; when a reading is greater than or equal to this @@ -301,16 +297,6 @@ class Probe(Base): def __str__(self): return self.description - def last_reading(self): - """ - Returns the reading which was taken most recently for this probe. - """ - session = orm.object_session(self) - return session.query(Reading)\ - .filter(Reading.probe == self)\ - .order_by(Reading.taken.desc())\ - .first() - def start_status(self, status, time): """ Update the "started" timestamp field for the given status. This is @@ -396,6 +382,7 @@ class Probe(Base): return self.error_timeout +@six.python_2_unicode_compatible class Reading(Base): """ Represents a single temperature reading from a tempmon probe. @@ -426,7 +413,6 @@ class Reading(Base): """, backref=orm.backref( 'readings', - order_by='Reading.taken', cascade='all, delete-orphan')) taken = sa.Column(sa.DateTime(), nullable=False, default=datetime.datetime.utcnow) diff --git a/rattail_tempmon/emails.py b/rattail_tempmon/emails.py index 4a3b5cf..11256c8 100644 --- a/rattail_tempmon/emails.py +++ b/rattail_tempmon/emails.py @@ -40,22 +40,17 @@ class TempmonBase(object): def sample_data(self, request): now = localtime(self.config) - client = tempmon.Client(config_key='testclient', hostname='testclient') - probe = tempmon.Probe(config_key='testprobe', description="Test Probe", - good_max_timeout=45) + client = model.TempmonClient(config_key='testclient', hostname='testclient') + probe = model.TempmonProbe(config_key='testprobe', description="Test Probe") client.probes.append(probe) return { 'client': client, 'probe': probe, - 'probe_url': '#', 'status': self.enum.TEMPMON_PROBE_STATUS[self.enum.TEMPMON_PROBE_STATUS_ERROR], - 'reading': tempmon.Reading(), + 'reading': model.TempmonReading(), 'taken': now, 'now': now, - 'status_since': now.strftime('%I:%M %p'), - 'status_since_delta': 'now', - 'recent_minutes': 90, - 'recent_readings': [], + 'probe_url': '#', } @@ -83,6 +78,19 @@ class tempmon_critical_low_temp(TempmonBase, Email): return data +class tempmon_critical_temp(TempmonBase, Email): + """ + Sent when a tempmon probe takes a reading which is "critical" in either the + high or low sense. + """ + default_subject = "Critical temperature detected" + + def sample_data(self, request): + data = super(tempmon_critical_temp, self).sample_data(request) + data['status'] = self.enum.TEMPMON_PROBE_STATUS[self.enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP] + return data + + class tempmon_error(TempmonBase, Email): """ Sent when a tempmon probe is noticed to have some error, i.e. no current readings. diff --git a/rattail_tempmon/problems.py b/rattail_tempmon/problems.py index 087ceb1..edb28af 100644 --- a/rattail_tempmon/problems.py +++ b/rattail_tempmon/problems.py @@ -37,13 +37,13 @@ def disabled_probes(config, progress=None): tempmon_session = TempmonSession() clients = tempmon_session.query(tempmon.Client)\ .filter(tempmon.Client.archived == False)\ - .filter(tempmon.Client.enabled == None)\ + .filter(tempmon.Client.enabled == False)\ .all() probes = tempmon_session.query(tempmon.Probe)\ .join(tempmon.Client)\ .filter(tempmon.Client.archived == False)\ - .filter(tempmon.Client.enabled != None)\ - .filter(tempmon.Probe.enabled == None)\ + .filter(tempmon.Client.enabled == True)\ + .filter(tempmon.Probe.enabled == False)\ .all() if clients or probes: send_email(config, 'tempmon_disabled_probes', { diff --git a/rattail_tempmon/server.py b/rattail_tempmon/server.py index a54614a..4c290bd 100644 --- a/rattail_tempmon/server.py +++ b/rattail_tempmon/server.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2018 Lance Edgar # # This file is part of Rattail. # @@ -24,10 +24,13 @@ Tempmon server daemon """ +from __future__ import unicode_literals, absolute_import + import time import datetime import logging +import six import humanize from sqlalchemy import orm from sqlalchemy.exc import OperationalError @@ -68,7 +71,7 @@ class TempmonServerDaemon(Daemon): try: clients = session.query(tempmon.Client)\ - .filter(tempmon.Client.enabled != None)\ + .filter(tempmon.Client.enabled == True)\ .filter(tempmon.Client.archived == False) for client in clients: self.check_readings_for_client(session, client) @@ -88,7 +91,7 @@ class TempmonServerDaemon(Daemon): # 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)): + 'could not connect to server: Connection refused' in six.text_type(error)): # only suppress logging for 3 failures, after that we let them go # TODO: should make the max attempts configurable @@ -96,7 +99,7 @@ class TempmonServerDaemon(Daemon): log_error = False log.debug("database connection failure #%s: %s", self.failed_checks, - str(error)) + six.text_type(error)) # send error email unless we're suppressing it for now if log_error: @@ -119,24 +122,10 @@ class TempmonServerDaemon(Daemon): # the client to be (possibly) offline. delay = client.delay or 60 cutoff = self.now - datetime.timedelta(seconds=delay + 60) - - # but if client was "just now" enabled, cutoff may not be quite fair. - # in this case we'll just skip checks until cutoff does seem fair. - if cutoff < client.enabled: - return - - # we make similar checks for each probe; if cutoff "is not fair" for - # any of them, we'll skip that probe check, and avoid marking client - # offline for this round, just to be safe online = False - cutoff_unfair = False for probe in client.enabled_probes(): - if cutoff < probe.enabled: - cutoff_unfair = True - elif self.check_readings_for_probe(session, probe, cutoff): + if self.check_readings_for_probe(session, probe, cutoff): online = True - if cutoff_unfair: - return # if client was previously marked online, but we have no "new" # readings, then let's look closer to see if it's been long enough to @@ -219,13 +208,13 @@ class TempmonServerDaemon(Daemon): and prev_status in (self.enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP, self.enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP) and prev_alert_sent): - self.send_email(status, 'tempmon_high_temp', data) + send_email(self.config, 'tempmon_high_temp', data) probe.status_alert_sent = self.now return # send email when things go back to normal (i.e. from any other status) if status == self.enum.TEMPMON_PROBE_STATUS_GOOD_TEMP and prev_alert_sent: - self.send_email(status, 'tempmon_good_temp', data) + send_email(self.config, 'tempmon_good_temp', data) probe.status_alert_sent = self.now return @@ -240,24 +229,8 @@ class TempmonServerDaemon(Daemon): return # delay even the first email, until configured threshold is reached - timeout = probe.timeout_for_status(status) - if timeout is None: - if status == self.enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP: - timeout = self.config.getint('rattail_tempmon', 'probe.default.critical_max_timeout', - default=0) - elif status == self.enum.TEMPMON_PROBE_STATUS_HIGH_TEMP: - timeout = self.config.getint('rattail_tempmon', 'probe.default.good_max_timeout', - default=0) - elif status == self.enum.TEMPMON_PROBE_STATUS_LOW_TEMP: - timeout = self.config.getint('rattail_tempmon', 'probe.default.good_min_timeout', - default=0) - elif status == self.enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP: - timeout = self.config.getint('rattail_tempmon', 'probe.default.critical_min_timeout', - default=0) - elif status == self.enum.TEMPMON_PROBE_STATUS_ERROR: - timeout = self.config.getint('rattail_tempmon', 'probe.default.error_timeout', - default=0) - timeout = datetime.timedelta(minutes=timeout or 0) + timeout = probe.timeout_for_status(status) or 0 + timeout = datetime.timedelta(minutes=timeout) started = probe.status_started(status) or probe.status_changed if (self.now - started) <= timeout: return diff --git a/rattail_tempmon/settings.py b/rattail_tempmon/settings.py deleted file mode 100644 index 18172bb..0000000 --- a/rattail_tempmon/settings.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2018 Lance Edgar -# -# 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. -# -# 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. -# -# You should have received a copy of the GNU General Public License along with -# Rattail. If not, see . -# -################################################################################ -""" -Rattail Tempmon Settings -""" - -from __future__ import unicode_literals, absolute_import - -from rattail.settings import Setting - - -############################## -# TempMon -############################## - -class rattail_tempmon_probe_default_critical_max_timeout(Setting): - """ - Default value to be used as Critical High Timeout value, for any probe - which does not have this timeout defined. - """ - group = "TempMon" - namespace = 'rattail_tempmon' - name = 'probe.default.critical_max_timeout' - data_type = int - - -class rattail_tempmon_probe_default_critical_min_timeout(Setting): - """ - Default value to be used as Critical Low Timeout value, for any probe which - does not have this timeout defined. - """ - group = "TempMon" - namespace = 'rattail_tempmon' - name = 'probe.default.critical_min_timeout' - data_type = int - - -class rattail_tempmon_probe_default_error_timeout(Setting): - """ - Default value to be used as Error Timeout value, for any probe which does - not have this timeout defined. - """ - group = "TempMon" - namespace = 'rattail_tempmon' - name = 'probe.default.error_timeout' - data_type = int - - -class rattail_tempmon_probe_default_good_max_timeout(Setting): - """ - Default value to be used as High Timeout value, for any probe which does - not have this timeout defined. - """ - group = "TempMon" - namespace = 'rattail_tempmon' - name = 'probe.default.good_max_timeout' - data_type = int - - -class rattail_tempmon_probe_default_good_min_timeout(Setting): - """ - Default value to be used as Low Timeout value, for any probe which does not - have this timeout defined. - """ - group = "TempMon" - namespace = 'rattail_tempmon' - name = 'probe.default.good_min_timeout' - data_type = int diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2c7d7cf --- /dev/null +++ b/setup.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2017 Lance Edgar +# +# 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. +# +# 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. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +setup script for rattail-tempmon +""" + +from __future__ import unicode_literals, absolute_import + +import os +from setuptools import setup, find_packages + + +here = os.path.abspath(os.path.dirname(__file__)) +exec(open(os.path.join(here, 'rattail_tempmon', '_version.py')).read()) +README = open(os.path.join(here, 'README.rst')).read() + + +requires = [ + # + # Version numbers within comments below have specific meanings. + # Basically the 'low' value is a "soft low," and 'high' a "soft high." + # In other words: + # + # If either a 'low' or 'high' value exists, the primary point to be + # made about the value is that it represents the most current (stable) + # version available for the package (assuming typical public access + # methods) whenever this project was started and/or documented. + # Therefore: + # + # If a 'low' version is present, you should know that attempts to use + # versions of the package significantly older than the 'low' version + # may not yield happy results. (A "hard" high limit may or may not be + # indicated by a true version requirement.) + # + # Similarly, if a 'high' version is present, and especially if this + # project has laid dormant for a while, you may need to refactor a bit + # when attempting to support a more recent version of the package. (A + # "hard" low limit should be indicated by a true version requirement + # when a 'high' version is present.) + # + # In any case, developers and other users are encouraged to play + # outside the lines with regard to these soft limits. If bugs are + # encountered then they should be filed as such. + # + # package # low high + + 'rattail[db]', # 0.7.46 + 'six', # 1.10.0 + 'sqlsoup', # 0.9.1 +] + + +setup( + name = "rattail-tempmon", + version = __version__, + author = "Lance Edgar", + author_email = "lance@edbob.org", + url = "https://rattailproject.org/", + license = "GNU GPL v3", + description = "Retail Software Framework - Temperature monitoring add-on", + long_description = README, + + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Topic :: Office/Business', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + install_requires = requires, + packages = find_packages(), + include_package_data = True, + + entry_points = { + 'rattail.commands': [ + 'export-hotcooler = rattail_tempmon.commands:ExportHotCooler', + 'purge-tempmon = rattail_tempmon.commands:PurgeTempmon', + 'tempmon-client = rattail_tempmon.commands:TempmonClient', + 'tempmon-problems = rattail_tempmon.commands:TempmonProblems', + 'tempmon-server = rattail_tempmon.commands:TempmonServer', + ], + 'rattail.config.extensions': [ + 'tempmon = rattail_tempmon.config:TempmonConfigExtension', + ], + }, +) diff --git a/tasks.py b/tasks.py index 497f721..09eca73 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2018 Lance Edgar # # This file is part of Rattail. # @@ -24,20 +24,17 @@ Tasks for 'rattail-tempmon' package """ -import os +from __future__ import unicode_literals, absolute_import + import shutil from invoke import task @task -def release(c): +def release(ctx): """ Release a new version of `rattail-tempmon` """ - if os.path.exists('dist'): - shutil.rmtree('dist') - if os.path.exists('rattail_tempmon.egg-info'): - shutil.rmtree('rattail_tempmon.egg-info') - c.run('python -m build --sdist') - c.run('twine upload dist/*') + shutil.rmtree('rattail_tempmon.egg-info') + ctx.run('python setup.py sdist --formats=gztar upload')