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 61%
rename from docs/OLDCHANGES.rst
rename to CHANGES.rst
index 8727078..9245574 100644
--- a/docs/OLDCHANGES.rst
+++ b/CHANGES.rst
@@ -2,100 +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)
-------------------
-
-* Add per-status timeouts and tracking for probe status.
-
-* Add appliance table, and probe "location" in that context.
-
-* Add image fields for Appliance table.
-
-
0.1.19 (2018-10-17)
-------------------
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..560cfb5 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.1.19'
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/796084026e5b_add_appliance.py b/rattail_tempmon/db/alembic/versions/796084026e5b_add_appliance.py
deleted file mode 100644
index 280c051..0000000
--- a/rattail_tempmon/db/alembic/versions/796084026e5b_add_appliance.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding: utf-8; -*-
-"""add appliance
-
-Revision ID: 796084026e5b
-Revises: b02c531caca5
-Create Date: 2018-10-19 17:28:34.146307
-
-"""
-
-from __future__ import unicode_literals, absolute_import
-
-# revision identifiers, used by Alembic.
-revision = '796084026e5b'
-down_revision = u'b02c531caca5'
-branch_labels = None
-depends_on = None
-
-from alembic import op
-import sqlalchemy as sa
-import rattail.db.types
-
-
-
-def upgrade():
-
- # appliance
- op.create_table('appliance',
- sa.Column('uuid', sa.String(length=32), nullable=False),
- sa.Column('name', sa.String(length=255), nullable=False),
- sa.Column('location', sa.String(length=255), nullable=True),
- sa.PrimaryKeyConstraint('uuid'),
- sa.UniqueConstraint('name', name=u'appliance_uq_name')
- )
-
- # probe
- op.add_column(u'probe', sa.Column('appliance_uuid', sa.String(length=32), nullable=True))
- op.add_column(u'probe', sa.Column('location', sa.String(length=255), nullable=True))
- op.create_foreign_key(u'probe_fk_appliance', 'probe', 'appliance', ['appliance_uuid'], ['uuid'])
-
-
-def downgrade():
-
- # probe
- op.drop_constraint(u'probe_fk_appliance', 'probe', type_='foreignkey')
- op.drop_column(u'probe', 'location')
- op.drop_column(u'probe', 'appliance_uuid')
-
- # appliance
- op.drop_table('appliance')
diff --git a/rattail_tempmon/db/alembic/versions/a2676d3dfc1e_add_appliance_images.py b/rattail_tempmon/db/alembic/versions/a2676d3dfc1e_add_appliance_images.py
deleted file mode 100644
index 45fb8ea..0000000
--- a/rattail_tempmon/db/alembic/versions/a2676d3dfc1e_add_appliance_images.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# -*- coding: utf-8; -*-
-"""add appliance images
-
-Revision ID: a2676d3dfc1e
-Revises: 796084026e5b
-Create Date: 2018-10-19 18:27:01.700943
-
-"""
-
-from __future__ import unicode_literals, absolute_import
-
-# revision identifiers, used by Alembic.
-revision = 'a2676d3dfc1e'
-down_revision = u'796084026e5b'
-branch_labels = None
-depends_on = None
-
-from alembic import op
-import sqlalchemy as sa
-import rattail.db.types
-
-
-
-def upgrade():
-
- # appliance
- op.add_column('appliance', sa.Column('appliance_type', sa.Integer(), nullable=True))
- op.add_column('appliance', sa.Column('image_normal', sa.LargeBinary(), nullable=True))
- op.add_column('appliance', sa.Column('image_raw', sa.LargeBinary(), nullable=True))
- op.add_column('appliance', sa.Column('image_thumbnail', sa.LargeBinary(), nullable=True))
-
-
-def downgrade():
-
- # appliance
- op.drop_column('appliance', 'image_thumbnail')
- op.drop_column('appliance', 'image_raw')
- op.drop_column('appliance', 'image_normal')
- op.drop_column('appliance', 'appliance_type')
diff --git a/rattail_tempmon/db/alembic/versions/b02c531caca5_add_more_timeouts.py b/rattail_tempmon/db/alembic/versions/b02c531caca5_add_more_timeouts.py
deleted file mode 100644
index 3b5074a..0000000
--- a/rattail_tempmon/db/alembic/versions/b02c531caca5_add_more_timeouts.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# -*- coding: utf-8; -*-
-"""add more timeouts
-
-Revision ID: b02c531caca5
-Revises: 5f2b87474433
-Create Date: 2018-10-19 13:51:54.422490
-
-"""
-
-from __future__ import unicode_literals, absolute_import
-
-# revision identifiers, used by Alembic.
-revision = 'b02c531caca5'
-down_revision = u'5f2b87474433'
-branch_labels = None
-depends_on = None
-
-from alembic import op
-import sqlalchemy as sa
-import rattail.db.types
-
-
-
-def upgrade():
-
- # probe
- op.add_column('probe', sa.Column('critical_max_started', sa.DateTime(), nullable=True))
- op.add_column('probe', sa.Column('critical_max_timeout', sa.Integer(), nullable=True))
- op.add_column('probe', sa.Column('critical_min_started', sa.DateTime(), nullable=True))
- op.add_column('probe', sa.Column('critical_min_timeout', sa.Integer(), nullable=True))
- op.add_column('probe', sa.Column('error_started', sa.DateTime(), nullable=True))
- op.add_column('probe', sa.Column('error_timeout', sa.Integer(), nullable=True))
- op.add_column('probe', sa.Column('good_max_started', sa.DateTime(), nullable=True))
- op.add_column('probe', sa.Column('good_max_timeout', sa.Integer(), nullable=True))
- op.add_column('probe', sa.Column('good_min_started', sa.DateTime(), nullable=True))
- op.add_column('probe', sa.Column('good_min_timeout', sa.Integer(), nullable=True))
-
-
-def downgrade():
-
- # probe
- op.drop_column('probe', 'good_min_timeout')
- op.drop_column('probe', 'good_min_started')
- op.drop_column('probe', 'good_max_timeout')
- op.drop_column('probe', 'good_max_started')
- op.drop_column('probe', 'error_timeout')
- op.drop_column('probe', 'error_started')
- op.drop_column('probe', 'critical_min_timeout')
- op.drop_column('probe', 'critical_min_started')
- op.drop_column('probe', 'critical_max_timeout')
- op.drop_column('probe', 'critical_max_started')
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..b64353e 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-2017 Lance Edgar
#
# This file is part of Rattail.
#
@@ -24,16 +24,15 @@
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
from rattail.db.model.core import ModelBase
@@ -41,45 +40,7 @@ from rattail.db.model.core import ModelBase
Base = declarative_base(cls=ModelBase)
-class Appliance(Base):
- """
- Represents an appliance which is monitored by tempmon.
- """
- __tablename__ = 'appliance'
- __table_args__ = (
- sa.UniqueConstraint('name', name='appliance_uq_name'),
- )
-
- uuid = uuid_column()
-
- name = sa.Column(sa.String(length=255), nullable=False, doc="""
- Human-friendly (and unique) name for the appliance.
- """)
-
- appliance_type = sa.Column(sa.Integer(), nullable=True, doc="""
- Code indicating which "type" of appliance this is.
- """)
-
- location = sa.Column(sa.String(length=255), nullable=True, doc="""
- Description of the appliance's physical location.
- """)
-
- image_raw = sa.Column(sa.LargeBinary(), nullable=True, doc="""
- Byte sequence of the raw image, as uploaded.
- """)
-
- image_normal = sa.Column(sa.LargeBinary(), nullable=True, doc="""
- Byte sequence of the normalized image, i.e. "reasonable" size.
- """)
-
- image_thumbnail = sa.Column(sa.LargeBinary(), nullable=True, doc="""
- Byte sequence of the thumbnail image.
- """)
-
- def __str__(self):
- return self.name
-
-
+@six.python_2_unicode_compatible
class Client(Base):
"""
Represents a tempmon client.
@@ -109,11 +70,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 +98,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.
@@ -144,7 +106,6 @@ class Probe(Base):
__tablename__ = 'probe'
__table_args__ = (
sa.ForeignKeyConstraint(['client_uuid'], ['client.uuid'], name='probe_fk_client'),
- sa.ForeignKeyConstraint(['appliance_uuid'], ['appliance.uuid'], name='probe_fk_appliance'),
sa.UniqueConstraint('config_key', name='probe_uq_config_key'),
)
@@ -164,130 +125,27 @@ class Probe(Base):
"""))
config_key = sa.Column(sa.String(length=50), nullable=False)
-
appliance_type = sa.Column(sa.Integer(), nullable=False)
-
- appliance_uuid = sa.Column(sa.String(length=32), nullable=True)
- appliance = orm.relationship(
- Appliance,
- doc="""
- Reference to the appliance which this probe monitors.
- """,
- backref=orm.backref(
- 'probes',
- order_by='Probe.description',
- doc="""
- List of probes which monitor this appliance.
- """))
-
- description = sa.Column(sa.String(length=255), nullable=False, doc="""
- General human-friendly description for the probe.
- """)
-
- location = sa.Column(sa.String(length=255), nullable=True, doc="""
- Description of the probe's physical location, relative to the appliance.
- """)
-
+ description = sa.Column(sa.String(length=255), nullable=False)
device_path = sa.Column(sa.String(length=255), nullable=True)
+ enabled = sa.Column(sa.Boolean(), nullable=False, default=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.
- """)
+ good_temp_min = sa.Column(sa.Integer(), nullable=False)
+ good_temp_max = sa.Column(sa.Integer(), nullable=False)
+ critical_temp_min = sa.Column(sa.Integer(), nullable=False)
+ critical_temp_max = sa.Column(sa.Integer(), nullable=False)
- critical_temp_max = sa.Column(sa.Integer(), nullable=False, doc="""
- Maximum high temperature; when a reading is greater than or equal to this
- value, the probe's status becomes "critical high temp".
- """)
-
- critical_max_started = sa.Column(sa.DateTime(), nullable=True, doc="""
- Timestamp when the probe readings started to indicate "critical high temp"
- status. This should be null unless the probe currently has that status.
- """)
-
- critical_max_timeout = sa.Column(sa.Integer(), nullable=True, doc="""
- Number of minutes the probe is allowed to have "critical high temp" status,
- before the first email alert is sent for that. If empty, there will be no
- delay and the first email will go out as soon as that status is reached.
- If set, should probably be a *low* number.
- """)
-
- good_temp_max = sa.Column(sa.Integer(), nullable=False, doc="""
- Maximum good temperature; when a reading is greater than or equal to this
- value, the probe's status becomes "high temp" (unless the reading also
- breaches the :attr:`critical_temp_max` threshold).
- """)
-
- good_max_timeout = sa.Column(sa.Integer(), nullable=True, doc="""
- Number of minutes the probe is allowed to have "high temp" status, before
- the first email alert is sent for that. This is typically meant to account
- for the length of the defrost cycle, so may be a rather large number.
- """)
-
- good_max_started = sa.Column(sa.DateTime(), nullable=True, doc="""
- Timestamp when the probe readings started to indicate "high temp" status.
- This should be null unless the probe currently has either "high temp" or
- "critical high temp" status.
- """)
-
- good_temp_min = sa.Column(sa.Integer(), nullable=False, doc="""
- Minimum good temperature; when a reading is less than or equal to this
- value, the probe's status becomes "low temp" (unless the reading also
- breaches the :attr:`critical_temp_min` threshold).
- """)
-
- good_min_timeout = sa.Column(sa.Integer(), nullable=True, doc="""
- Number of minutes the probe is allowed to have "low temp" status, before
- the first email alert is sent for that.
- """)
-
- good_min_started = sa.Column(sa.DateTime(), nullable=True, doc="""
- Timestamp when the probe readings started to indicate "low temp" status.
- This should be null unless the probe currently has either "low temp" or
- "critical low temp" status.
- """)
-
- critical_temp_min = sa.Column(sa.Integer(), nullable=False, doc="""
- Minimum low temperature; when a reading is less than or equal to this
- value, the probe's status becomes "critical low temp". If empty, there
- will be no delay and the first email will go out as soon as that status is
- reached.
- """)
-
- critical_min_started = sa.Column(sa.DateTime(), nullable=True, doc="""
- Timestamp when the probe readings started to indicate "critical low temp"
- status. This should be null unless the probe currently has that status.
- """)
-
- critical_min_timeout = sa.Column(sa.Integer(), nullable=True, doc="""
- Number of minutes the probe is allowed to have "critical low temp" status,
- before the first email alert is sent for that. If empty, there will be no
- delay and the first email will go out as soon as that status is reached.
- """)
-
- error_started = sa.Column(sa.DateTime(), nullable=True, doc="""
- Timestamp when the probe readings started to indicate "error" status. This
- should be null unless the probe currently has that status.
- """)
-
- error_timeout = sa.Column(sa.Integer(), nullable=True, doc="""
- Number of minutes the probe is allowed to have "error" status, before the
- first email alert is sent for that. If empty, there will be no delay and
- the first email will go out as soon as that status is reached.
- """)
-
- # TODO: deprecate / remove this
therm_status_timeout = sa.Column(sa.Integer(), nullable=False, doc="""
- NOTE: This field is deprecated; please set the value for High Timeout instead.
+ Number of minutes the temperature is allowed to be "high" before the first
+ "high temp" email alert is sent. This is typically meant to account for
+ the length of the defrost cycle. Note that this does *not* affect the
+ "critical temp" emails; those are sent as soon as critical temp is reached.
""")
status_alert_timeout = sa.Column(sa.Integer(), nullable=False, doc="""
- Number of minutes between successive status alert emails. These alerts
- will continue to be sent until the status changes. Note that the *first*
- alert for a given status will be delayed according to the timeout for that
- status.
+ Number of minutes between successive "high/critical temp" emails. These
+ alerts will continue to be sent until the temperature returns to normal
+ range.
""")
notes = sa.Column(sa.Text(), nullable=True, doc="""
@@ -301,101 +159,8 @@ 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
- used to track e.g. when we cross the "high temp" threshold, as a
- separate event from when the "critical high temp" threshold is reached.
-
- Note that in addition to setting the appropriate timestamp field, this
- also will clear out other timestamp fields, according to the specific
- (new) status.
- """
- if status in (enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP,
- enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP):
- self.critical_max_started = time
- # note, we don't clear out "high temp" time
- self.good_min_started = None
- self.critical_min_started = None
- self.error_started = None
-
- elif status == enum.TEMPMON_PROBE_STATUS_HIGH_TEMP:
- self.critical_max_started = None
- self.good_max_started = time
- self.good_min_started = None
- self.critical_min_started = None
- self.error_started = None
-
- elif status == enum.TEMPMON_PROBE_STATUS_LOW_TEMP:
- self.critical_max_started = None
- self.good_max_started = None
- self.good_min_started = time
- self.critical_min_started = None
- self.error_started = None
-
- elif status == enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP:
- self.critical_max_started = None
- self.good_max_started = None
- # note, we don't clear out "low temp" time
- self.critical_min_started = time
- self.error_started = None
-
- elif status == enum.TEMPMON_PROBE_STATUS_ERROR:
- # note, we don't clear out any other status times
- self.error_started = time
-
- def status_started(self, status):
- """
- Return the timestamp indicating when the given status started.
- """
- if status in (enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP,
- enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP):
- return self.critical_max_started
-
- elif status == enum.TEMPMON_PROBE_STATUS_HIGH_TEMP:
- return self.good_max_started
-
- elif status == enum.TEMPMON_PROBE_STATUS_LOW_TEMP:
- return self.good_min_started
-
- elif status == enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP:
- return self.critical_min_started
-
- elif status == enum.TEMPMON_PROBE_STATUS_ERROR:
- return self.error_started
-
- def timeout_for_status(self, status):
- """
- Returns the timeout value for the given status. This is be the number
- of minutes by which we should delay the initial email for the status.
- """
- if status in (enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP,
- enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP):
- return self.critical_max_timeout
-
- elif status == enum.TEMPMON_PROBE_STATUS_HIGH_TEMP:
- return self.good_max_timeout or self.therm_status_timeout
-
- elif status == enum.TEMPMON_PROBE_STATUS_LOW_TEMP:
- return self.good_min_timeout or self.therm_status_timeout
-
- elif status == enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP:
- return self.critical_min_timeout
-
- elif status == enum.TEMPMON_PROBE_STATUS_ERROR:
- return self.error_timeout
-
+@six.python_2_unicode_compatible
class Reading(Base):
"""
Represents a single temperature reading from a tempmon probe.
@@ -426,7 +191,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..79eeab5 100644
--- a/rattail_tempmon/emails.py
+++ b/rattail_tempmon/emails.py
@@ -40,46 +40,30 @@ 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': '#',
}
-class tempmon_critical_high_temp(TempmonBase, Email):
+class tempmon_critical_temp(TempmonBase, Email):
"""
- Sent when a tempmon probe takes a "critical high" temperature reading.
+ Sent when a tempmon probe takes a reading which is "critical" in either the
+ high or low sense.
"""
- default_subject = "CRITICAL HIGH Temperature"
-
+ default_subject = "Critical temperature detected"
+
def sample_data(self, request):
- data = super(tempmon_critical_high_temp, self).sample_data(request)
- data['status'] = self.enum.TEMPMON_PROBE_STATUS[self.enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP]
- return data
-
-
-class tempmon_critical_low_temp(TempmonBase, Email):
- """
- Sent when a tempmon probe takes a "critical low" temperature reading.
- """
- default_subject = "CRITICAL LOW Temperature"
-
- def sample_data(self, request):
- data = super(tempmon_critical_low_temp, self).sample_data(request)
- data['status'] = self.enum.TEMPMON_PROBE_STATUS[self.enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP]
+ 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
@@ -108,7 +92,7 @@ class tempmon_good_temp(TempmonBase, Email):
Sent whenever a tempmon probe first takes a "good temp" reading, after
having previously had some bad reading(s).
"""
- default_subject = "OK Temperature"
+ default_subject = "Good temperature detected"
def sample_data(self, request):
data = super(tempmon_good_temp, self).sample_data(request)
@@ -121,7 +105,7 @@ class tempmon_high_temp(TempmonBase, Email):
Sent when a tempmon probe takes a reading which is above the "maximum good
temp" range, but still below the "critically high temp" threshold.
"""
- default_subject = "HIGH Temperature"
+ default_subject = "High temperature detected"
def sample_data(self, request):
data = super(tempmon_high_temp, self).sample_data(request)
@@ -134,7 +118,7 @@ class tempmon_low_temp(TempmonBase, Email):
Sent when a tempmon probe takes a reading which is below the "minimum good
temp" range, but still above the "critically low temp" threshold.
"""
- default_subject = "LOW Temperature"
+ default_subject = "Low temperature detected"
def sample_data(self, request):
data = super(tempmon_low_temp, self).sample_data(request)
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..507d02c 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) and not online:
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
@@ -158,6 +147,7 @@ class TempmonServerDaemon(Daemon):
'now': localtime(self.config, self.now, from_utc=True),
})
+
def check_readings_for_probe(self, session, probe, cutoff):
"""
Check readings for the given probe, within the time window defined by
@@ -171,21 +161,18 @@ class TempmonServerDaemon(Daemon):
.first()
if reading:
- # is reading above critical max?
- if reading.degrees_f >= probe.critical_temp_max:
- self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP, reading)
+ # is reading below critical min, or above critical max?
+ if (reading.degrees_f <= probe.critical_temp_min or
+ reading.degrees_f >= probe.critical_temp_max):
+ self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP, reading)
- # is reading above good max?
- elif reading.degrees_f >= probe.good_temp_max:
- self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_HIGH_TEMP, reading)
-
- # is reading below good min?
- elif reading.degrees_f <= probe.good_temp_min:
+ # is reading below "good" min?
+ elif reading.degrees_f < probe.good_temp_min:
self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_LOW_TEMP, reading)
- # is reading below critical min?
- elif reading.degrees_f <= probe.critical_temp_min:
- self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP, reading)
+ # is reading above "good" max?
+ elif reading.degrees_f > probe.good_temp_max:
+ self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_HIGH_TEMP, reading)
else: # temp is good
self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_GOOD_TEMP, reading)
@@ -209,25 +196,13 @@ class TempmonServerDaemon(Daemon):
prev_alert_sent = probe.status_alert_sent
if probe.status != status:
probe.status = status
- probe.start_status(status, self.now)
probe.status_changed = self.now
probe.status_alert_sent = None
- # send "high temp" email if previous status was critical, even if
- # we haven't been high for that long overall
- if (status == self.enum.TEMPMON_PROBE_STATUS_HIGH_TEMP
- 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)
- probe.status_alert_sent = self.now
- return
-
- # send email when things go back to normal (i.e. from any other status)
+ # send email when things go back to normal, after being bad
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
# no (more) email if status is good
if status == self.enum.TEMPMON_PROBE_STATUS_GOOD_TEMP:
@@ -240,55 +215,19 @@ 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)
- started = probe.status_started(status) or probe.status_changed
- if (self.now - started) <= timeout:
- return
-
- msgtypes = {
- self.enum.TEMPMON_PROBE_STATUS_CRITICAL_HIGH_TEMP : 'tempmon_critical_high_temp',
- self.enum.TEMPMON_PROBE_STATUS_HIGH_TEMP : 'tempmon_high_temp',
- self.enum.TEMPMON_PROBE_STATUS_LOW_TEMP : 'tempmon_low_temp',
- self.enum.TEMPMON_PROBE_STATUS_CRITICAL_LOW_TEMP : 'tempmon_critical_low_temp',
- self.enum.TEMPMON_PROBE_STATUS_ERROR : 'tempmon_error',
- }
-
- self.send_email(status, msgtypes[status], data)
-
- # maybe send more emails if config said so
- for msgtype in self.extra_emails:
- self.send_email(status, msgtype, data)
-
- probe.status_alert_sent = self.now
-
- def send_email(self, status, template, data):
- probe = data['probe']
- started = probe.status_started(status) or probe.status_changed
+ # unless we have a critical status
+ if status != self.enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP:
+ timeout = datetime.timedelta(minutes=probe.therm_status_timeout)
+ if (self.now - probe.status_changed) <= timeout:
+ return
# determine URL for probe, if possible
url = self.config.get('tailbone', 'url.tempmon.probe', default='#')
data['probe_url'] = url.format(uuid=probe.uuid)
- since = localtime(self.config, started, from_utc=True)
+ since = localtime(self.config, probe.status_changed, from_utc=True)
data['status_since'] = since.strftime('%I:%M %p')
- data['status_since_delta'] = humanize.naturaltime(self.now - started)
+ data['status_since_delta'] = humanize.naturaltime(self.now - probe.status_changed)
# fetch last 90 minutes of readings
session = orm.object_session(probe)
@@ -302,7 +241,20 @@ class TempmonServerDaemon(Daemon):
data['recent_readings'] = readings
data['pretty_time'] = lambda dt: localtime(self.config, dt, from_utc=True).strftime('%Y-%m-%d %I:%M %p')
- send_email(self.config, template, data)
+ msgtypes = {
+ self.enum.TEMPMON_PROBE_STATUS_LOW_TEMP : 'tempmon_low_temp',
+ self.enum.TEMPMON_PROBE_STATUS_HIGH_TEMP : 'tempmon_high_temp',
+ self.enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP : 'tempmon_critical_temp',
+ self.enum.TEMPMON_PROBE_STATUS_ERROR : 'tempmon_error',
+ }
+
+ send_email(self.config, msgtypes[status], data)
+
+ # maybe send more emails if config said so
+ for msgtype in self.extra_emails:
+ send_email(self.config, msgtype, data)
+
+ probe.status_alert_sent = self.now
def make_daemon(config, pidfile=None):
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
- This is an alert from ${probe}!
- The status of ${probe} is: ${status}.
- The current temperature is: ${reading.degrees_f}.
- The temperature should never be this high.
- Investigate Immediately!
-
- Notes:
- Frozen food that is above 40 degrees needs to be thrown away
- if it remains at that temperature for two hours or more.
-
- Check out this USDA link for useful information -
-
- This email will repeat every ${probe.status_alert_timeout} minutes until the issue
- has been resolved.
-
-
-
diff --git a/rattail_tempmon/templates/mail/tempmon_critical_high_temp.html.mako b/rattail_tempmon/templates/mail/tempmon_critical_temp.html.mako
similarity index 100%
rename from rattail_tempmon/templates/mail/tempmon_critical_high_temp.html.mako
rename to rattail_tempmon/templates/mail/tempmon_critical_temp.html.mako
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