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