Compare commits

...

51 commits

Author SHA1 Message Date
Lance Edgar f56cb41e69 docs: update project links, kallithea -> forgejo 2024-09-14 12:12:40 -05:00
Lance Edgar 07dda66bae docs: use markdown for readme file 2024-09-13 18:22:48 -05:00
Lance Edgar 949c9ee5a1 bump: version 0.4.5 → 0.4.6 2024-08-19 08:44:10 -05:00
Lance Edgar fa4cb5dc9a fix: avoid deprecated base class for config extension 2024-08-16 10:10:13 -05:00
Lance Edgar 7fe5e9aea6 bump: version 0.4.4 → 0.4.5 2024-07-02 01:23:02 -05:00
Lance Edgar 8021ac818e fix: fix signature for calls to get_engines() 2024-07-02 01:22:37 -05:00
Lance Edgar 55c84c6efe bump: version 0.4.3 → 0.4.4 2024-07-02 00:28:18 -05:00
Lance Edgar 56d7a48e45 fix: avoid deprecated function for engine config 2024-07-02 00:27:59 -05:00
Lance Edgar fe0840d3e0 bump: version 0.4.2 → 0.4.3 2024-07-01 23:22:07 -05:00
Lance Edgar f36759dc48 fix: remove references, dependency for six package 2024-07-01 16:40:46 -05:00
Lance Edgar ff0af6732a bump: version 0.4.1 → 0.4.2 2024-07-01 14:11:57 -05:00
Lance Edgar 6257362534 fix: remove legacy command definitions 2024-07-01 12:20:48 -05:00
Lance Edgar be4d6bfe4d bump: version 0.4.0 → 0.4.1 2024-06-14 17:37:04 -05:00
Lance Edgar f4682c9070 fix: fallback to importlib_metadata on older python 2024-06-14 17:35:23 -05:00
Lance Edgar eb8962003c bump: version 0.3.0 → 0.4.0 2024-06-10 21:23:00 -05:00
Lance Edgar 6b4280a6aa feat: switch from setup.cfg to pyproject.toml + hatchling 2024-06-10 21:21:19 -05:00
Lance Edgar 784f75ac3f Fix default dist filename for release task
not sure why this fix was needed, did setuptools behavior change?
2024-05-30 11:15:07 -05:00
Lance Edgar f01faaf2f9 Update changelog 2024-05-30 11:14:16 -05:00
Lance Edgar 3fde33ac84 Fix pidfile args in typer commands 2024-05-29 07:27:22 -05:00
Lance Edgar 3f46ee6a30 Fix typo for purge command 2024-05-17 09:38:55 -05:00
Lance Edgar 9a39db4546 Add typer equivalents for rattail commands 2024-05-16 20:55:42 -05:00
Lance Edgar d8b865da71 Update changelog 2023-11-30 22:24:15 -06:00
Lance Edgar 7a11ee7ad7 Update subcommand entry point group names, per wuttjamaican 2023-11-22 18:05:58 -06:00
Lance Edgar e82e714417 Update changelog 2023-05-16 17:40:36 -05:00
Lance Edgar a16f2ba718 Replace setup.py contents with setup.cfg 2023-05-16 13:21:26 -05:00
Lance Edgar b887875f80 Update changelog 2023-02-12 11:22:06 -06:00
Lance Edgar 995e0dde0a Refactor Query.get() => Session.get() per SQLAlchemy 1.4 2023-02-11 22:24:56 -06:00
Lance Edgar f7f60eff85 Avoid error when re-running release task 2023-02-10 21:12:57 -06:00
Lance Edgar acfc7f7d80 Update changelog 2023-02-10 21:09:51 -06:00
Lance Edgar 304cec9dd5 Address a warning from SQLAlchemy for declarative_base
as of 1.4 that has moved
2023-02-08 10:57:04 -06:00
Lance Edgar fea643145a Officially drop support for python2 2023-02-08 10:53:27 -06:00
Lance Edgar a45a0b44d5 Use build module instead of invoking setup.py for release 2022-08-06 21:10:57 -05:00
Lance Edgar 1efdd9debd Update changelog 2022-08-06 21:10:11 -05:00
Lance Edgar 1ddeb8a030 Register email profiles provided by this pkg 2022-08-06 21:09:27 -05:00
Lance Edgar 28ecdda0e6 Update changelog 2020-09-22 19:56:10 -05:00
Lance Edgar 4eebd454d5 Declare sort order for Appliance.probes relationship 2020-04-04 19:30:37 -05:00
Lance Edgar 1b03841c7f Remove config for deprecated 'tempmon_critical_temp' email
we now have critical_high and critical_low as separate emails
2019-06-13 12:06:49 -05:00
Lance Edgar 873cd3def9 Update changelog 2019-04-23 22:22:07 -05:00
Lance Edgar 353abcc172 Make sure we use zero as fallback/default timeout values 2019-04-09 12:43:00 -05:00
Lance Edgar 8187c9532f Update changelog 2019-01-28 15:48:57 -06:00
Lance Edgar cf27af81d4 Modify tempmon server logic to take "unfair" time windows into account
when a client or probe first are (re-)enabled, we can't expect to have readings
within the time window we'd normally be checking.  previously we'd get false
alarms about "probe error status" etc. when this happened; hopefully no longer!
2019-01-25 19:49:46 -06:00
Lance Edgar f31a0c4c22 Convert enabled for Client, Probe to use datetime instead of boolean
value is null if disabled, else timestamp of when it was last enabled
2019-01-25 19:33:49 -06:00
Lance Edgar c45baaed5e Add more template context for email previews
just to keep things from breaking outright..although this hasn't been given
much attention still, numbers may not make total sense
2018-10-31 17:20:35 -05:00
Lance Edgar 3b14a0b288 Update changelog 2018-10-25 09:01:19 -05:00
Lance Edgar 7212b07504 Fix bug when sending certain emails while checking probe readings
use common method to add more context for email template
2018-10-25 09:00:17 -05:00
Lance Edgar ad3e647160 Update changelog 2018-10-24 19:20:44 -05:00
Lance Edgar 8220082359 Add try/catch for client's "read temp" logic
this can isolate an error for a certain probe, so that other probes can go
ahead and take their readings during each client run.  that way only the bad
one is marked as "error" status by the server
2018-10-23 17:39:18 -05:00
Lance Edgar b644818eef Don't mark client as online unless it's also enabled 2018-10-23 17:38:38 -05:00
Lance Edgar 30f0fe0a84 Add "default" probe timeout logic for server readings check
this way we don't have to set those timeouts on every single probe
2018-10-23 10:25:08 -05:00
Lance Edgar 1f8507508a Make dummy probe use tighter pattern for random readings
to make for a more reasonable-looking graph
2018-10-20 04:17:42 -05:00
Lance Edgar 44d012b3fd Update release task to use twine for upload 2018-10-19 20:31:49 -05:00
17 changed files with 632 additions and 310 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
*~
*.pyc
dist/
rattail_tempmon.egg-info/

53
CHANGELOG.md Normal file
View file

@ -0,0 +1,53 @@
# 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.

View file

@ -1,6 +1,5 @@
rattail-tempmon
===============
# rattail-tempmon
Rattail is a retail software framework, released under the GNU General Public
License.
@ -8,6 +7,5 @@ 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`_ for more information.
.. _home page: https://rattailproject.org/
Please see Rattail's [home page](https://rattailproject.org/) for more
information.

View file

@ -2,6 +2,90 @@
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)
------------------

52
pyproject.toml Normal file
View file

@ -0,0 +1,52 @@
[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

View file

@ -1,3 +1,9 @@
# -*- coding: utf-8; -*-
__version__ = '0.2.0'
try:
from importlib.metadata import version
except ImportError:
from importlib_metadata import version
__version__ = version('rattail-tempmon')

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,15 +24,12 @@
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
@ -86,16 +83,16 @@ class TempmonClient(Daemon):
session = Session()
try:
client = session.query(tempmon.Client).get(client_uuid)
client = session.get(tempmon.Client, client_uuid)
self.delay = client.delay or 60
if client.enabled:
for probe in client.enabled_probes():
self.take_reading(session, probe)
session.flush()
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
@ -111,7 +108,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 six.text_type(error)):
'could not connect to server: Connection refused' in str(error)):
# only suppress logging for 3 failures, after that we let them go
# TODO: should make the max attempts configurable
@ -119,7 +116,7 @@ class TempmonClient(Daemon):
log_error = False
log.debug("database connection failure #%s: %s",
self.failed_checks,
six.text_type(error))
str(error))
# send error email unless we're suppressing it for now
if log_error:
@ -137,22 +134,28 @@ class TempmonClient(Daemon):
Take a single reading and add to Rattail database.
"""
reading = tempmon.Reading()
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)
try:
reading.degrees_f = self.read_temp(probe)
else: # we have a good reading
reading.client = probe.client
reading.probe = probe
reading.taken = datetime.datetime.utcnow()
session.add(reading)
return reading
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
def read_temp(self, probe):
"""
@ -181,7 +184,18 @@ class TempmonClient(Daemon):
return therm_file.readlines()
def random_temp(self, probe):
temp = random.uniform(probe.critical_temp_min - 5, probe.critical_temp_max + 5)
last_reading = probe.last_reading()
if last_reading:
volatility = 2
# try to keep somewhat of a tight pattern, so graphs look reasonable
last_degrees = float(last_reading.degrees_f)
temp = random.uniform(last_degrees - volatility * 3, last_degrees + volatility * 3)
if temp > (probe.critical_temp_max + volatility * 2):
temp -= volatility
elif temp < (probe.critical_temp_min - volatility * 2):
temp += volatility
else:
temp = random.uniform(probe.critical_temp_min - 5, probe.critical_temp_max + 5)
return round(temp, 4)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,137 +24,157 @@
Tempmon commands
"""
from __future__ import unicode_literals, absolute_import
import datetime
import logging
from enum import Enum
from pathlib import Path
from rattail import commands
from rattail.time import localtime, make_utc
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
log = logging.getLogger(__name__)
class ExportHotCooler(commands.ImportSubcommand):
class ServiceAction(str, Enum):
start = 'start'
stop = 'stop'
@rattail_typer.command()
@importer_command
def export_hotcooler(
ctx: typer.Context,
**kwargs
):
"""
Export data from Rattail-Tempmon to HotCooler
"""
name = 'export-hotcooler'
description = __doc__.strip()
handler_spec = 'rattail_tempmon.hotcooler.importing.tempmon:FromTempmonToHotCooler'
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)
class PurgeTempmon(commands.Subcommand):
@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,
):
"""
Purge stale data from Tempmon database
"""
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()
config = ctx.parent.rattail_config
progress = ctx.parent.rattail_progress
do_purge(config, keep_days, dry_run=dry_run, progress=progress)
class TempmonClient(commands.Subcommand):
@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,
):
"""
Manage the tempmon-client daemon
"""
name = 'tempmon-client'
description = __doc__.strip()
from rattail_tempmon.client import make_daemon
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()
config = ctx.parent.rattail_config
daemon = make_daemon(config, pidfile)
if action == 'start':
daemon.start(daemonize)
elif action == 'stop':
daemon.stop()
class TempmonServer(commands.Subcommand):
"""
Manage the tempmon-server daemon
"""
name = 'tempmon-server'
description = __doc__.strip()
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()
class TempmonProblems(commands.Subcommand):
@rattail_typer.command()
def tempmon_problems(
ctx: typer.Context,
):
"""
Email report(s) of various Tempmon data problems
"""
name = 'tempmon-problems'
description = __doc__.strip()
from rattail_tempmon import problems
def run(self, args):
from rattail_tempmon import problems
config = ctx.parent.rattail_config
progress = ctx.parent.rattail_progress
problems.disabled_probes(config, progress=progress)
problems.disabled_probes(self.config, progress=self.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,
):
"""
Manage the tempmon-server daemon
"""
from rattail_tempmon.server import make_daemon
config = ctx.parent.rattail_config
daemon = make_daemon(config, pidfile)
if action == 'start':
daemon.start(daemonize)
elif action == '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
app = config.get_app()
cutoff = app.today() - datetime.timedelta(days=keep_days)
cutoff = app.localtime(datetime.datetime.combine(cutoff, datetime.time(0)))
session = Session()
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)

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,14 +24,13 @@
Tempmon config extension
"""
from __future__ import unicode_literals, absolute_import
from wuttjamaican.db import get_engines
from wuttjamaican.conf import WuttaConfigExtension
from rattail.config import ConfigExtension
from rattail.db.config import get_engines
from rattail_tempmon.db import Session
class TempmonConfigExtension(ConfigExtension):
class TempmonConfigExtension(WuttaConfigExtension):
"""
Config extension for tempmon; adds tempmon DB engine/Session etc. Expects
something like this in your config:
@ -53,10 +52,10 @@ class TempmonConfigExtension(ConfigExtension):
def configure(self, config):
# tempmon
config.tempmon_engines = get_engines(config, section='rattail_tempmon.db')
config.tempmon_engines = get_engines(config, 'rattail_tempmon.db')
config.tempmon_engine = config.tempmon_engines.get('default')
Session.configure(bind=config.tempmon_engine)
# hotcooler
config.hotcooler_engines = get_engines(config, section='hotcooler.db')
config.hotcooler_engines = get_engines(config, 'hotcooler.db')
config.hotcooler_engine = config.hotcooler_engines.get('default')

View file

@ -0,0 +1,80 @@
# -*- 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)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar
# Copyright © 2010-2023 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
from sqlalchemy.ext.declarative import declarative_base
try:
from sqlalchemy.orm import declarative_base
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from rattail import enum
from rattail.db.model import uuid_column
@ -41,7 +41,6 @@ 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.
@ -81,7 +80,6 @@ class Appliance(Base):
return self.name
@six.python_2_unicode_compatible
class Client(Base):
"""
Represents a tempmon client.
@ -111,11 +109,11 @@ class Client(Base):
Any arbitrary notes for the client.
""")
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.
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.
""")
online = sa.Column(sa.Boolean(), nullable=False, default=False, doc="""
@ -139,7 +137,6 @@ 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.
@ -178,6 +175,7 @@ class Probe(Base):
""",
backref=orm.backref(
'probes',
order_by='Probe.description',
doc="""
List of probes which monitor this appliance.
"""))
@ -191,7 +189,13 @@ class Probe(Base):
""")
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.
""")
critical_temp_max = sa.Column(sa.Integer(), nullable=False, doc="""
Maximum high temperature; when a reading is greater than or equal to this
@ -297,6 +301,16 @@ 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
@ -382,7 +396,6 @@ class Probe(Base):
return self.error_timeout
@six.python_2_unicode_compatible
class Reading(Base):
"""
Represents a single temperature reading from a tempmon probe.
@ -413,6 +426,7 @@ 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)

View file

@ -40,17 +40,22 @@ class TempmonBase(object):
def sample_data(self, request):
now = localtime(self.config)
client = model.TempmonClient(config_key='testclient', hostname='testclient')
probe = model.TempmonProbe(config_key='testprobe', description="Test Probe")
client = tempmon.Client(config_key='testclient', hostname='testclient')
probe = tempmon.Probe(config_key='testprobe', description="Test Probe",
good_max_timeout=45)
client.probes.append(probe)
return {
'client': client,
'probe': probe,
'probe_url': '#',
'status': self.enum.TEMPMON_PROBE_STATUS[self.enum.TEMPMON_PROBE_STATUS_ERROR],
'reading': model.TempmonReading(),
'reading': tempmon.Reading(),
'taken': now,
'now': now,
'probe_url': '#',
'status_since': now.strftime('%I:%M %p'),
'status_since_delta': 'now',
'recent_minutes': 90,
'recent_readings': [],
}
@ -78,19 +83,6 @@ 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.

View file

@ -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 == False)\
.filter(tempmon.Client.enabled == None)\
.all()
probes = tempmon_session.query(tempmon.Probe)\
.join(tempmon.Client)\
.filter(tempmon.Client.archived == False)\
.filter(tempmon.Client.enabled == True)\
.filter(tempmon.Probe.enabled == False)\
.filter(tempmon.Client.enabled != None)\
.filter(tempmon.Probe.enabled == None)\
.all()
if clients or probes:
send_email(config, 'tempmon_disabled_probes', {

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,13 +24,10 @@
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
@ -71,7 +68,7 @@ class TempmonServerDaemon(Daemon):
try:
clients = session.query(tempmon.Client)\
.filter(tempmon.Client.enabled == True)\
.filter(tempmon.Client.enabled != None)\
.filter(tempmon.Client.archived == False)
for client in clients:
self.check_readings_for_client(session, client)
@ -91,7 +88,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 six.text_type(error)):
'could not connect to server: Connection refused' in str(error)):
# only suppress logging for 3 failures, after that we let them go
# TODO: should make the max attempts configurable
@ -99,7 +96,7 @@ class TempmonServerDaemon(Daemon):
log_error = False
log.debug("database connection failure #%s: %s",
self.failed_checks,
six.text_type(error))
str(error))
# send error email unless we're suppressing it for now
if log_error:
@ -122,10 +119,24 @@ 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 self.check_readings_for_probe(session, probe, cutoff):
if cutoff < probe.enabled:
cutoff_unfair = True
elif 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
@ -208,13 +219,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):
send_email(self.config, 'tempmon_high_temp', data)
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)
if status == self.enum.TEMPMON_PROBE_STATUS_GOOD_TEMP and prev_alert_sent:
send_email(self.config, 'tempmon_good_temp', data)
self.send_email(status, 'tempmon_good_temp', data)
probe.status_alert_sent = self.now
return
@ -229,8 +240,24 @@ class TempmonServerDaemon(Daemon):
return
# delay even the first email, until configured threshold is reached
timeout = probe.timeout_for_status(status) or 0
timeout = datetime.timedelta(minutes=timeout)
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

View file

@ -0,0 +1,88 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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

111
setup.py
View file

@ -1,111 +0,0 @@
# -*- 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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',
],
},
)

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,17 +24,20 @@
Tasks for 'rattail-tempmon' package
"""
from __future__ import unicode_literals, absolute_import
import os
import shutil
from invoke import task
@task
def release(ctx):
def release(c):
"""
Release a new version of `rattail-tempmon`
"""
shutil.rmtree('rattail_tempmon.egg-info')
ctx.run('python setup.py sdist --formats=gztar upload')
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/*')