Compare commits

..

No commits in common. "master" and "v0.1.7" have entirely different histories.

39 changed files with 525 additions and 1929 deletions

3
.gitignore vendored
View file

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

View file

@ -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.

56
CHANGES.rst Normal file
View file

@ -0,0 +1,56 @@
CHANGELOG
=========
0.1.7 (2017-06-01)
------------------
* Add ``rattail export-hotcooler`` command, for initial hotcooler support
* Add client error logging in case committing session fails..
0.1.6 (2017-02-09)
------------------
* Add configurable delay per client; improve client try/catch
0.1.5 (2016-12-12)
------------------
* Add config for "good temp" email
0.1.4 (2016-12-11)
------------------
* Hopefully fix alert logic when status becomes good
0.1.3 (2016-12-10)
------------------
* Add email config for tempmon-server alerts
* Add mail templates to project manifest
0.1.2 (2016-12-10)
------------------
* Add support for dummy probes (random temp data)
* Add mail templates, plus initial status alert delay for probes
0.1.1 (2016-12-05)
------------------
* Fix import bug in server daemon
0.1.0 (2016-12-05)
------------------
* Initial release.

View file

@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@ -7,17 +7,15 @@
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program 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.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -1,11 +0,0 @@
# rattail-tempmon
Rattail is a retail software framework, released under the GNU General Public
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.

13
README.rst Normal file
View file

@ -0,0 +1,13 @@
rattail-tempmon
===============
Rattail is a retail software framework, released under the GNU Affero General
Public 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/

View file

@ -1,256 +0,0 @@
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)
-------------------
* Add probe URL to email template context.
* Leverage common "now" value when sending emails from server.
* Fix logic bug when checking readings for client.
0.1.18 (2018-10-09)
-------------------
* Log our supposed hostname on client startup.
* Log error on client, when probe takes a 185.0 reading.
* Improve docstrings for some model attributes (for Tailbone).
* Make server more tolerant of database restart.
* Make client more tolerant of database restart.
* Add "status since" to template context for email alerts.
* Add "recent readings" to email template context.
0.1.17 (2018-10-04)
-------------------
* Include client key in disabled probe list email.
* Don't let server mark client as offline until readings fail 3 times in a row.
* Add ``Client.archived`` flag, ignore archived for "disabled probes" check.
* Add notes field to client and probe tables.
* Add ``Client.disk_type`` to track SD card vs. USB.
0.1.16 (2018-02-07)
-------------------
* Send first alert "immediately" if critical temp status.
* Send email alert when tempmon server marks a client as offline.
0.1.15 (2017-11-19)
-------------------
* Add problem report for disabled clients/probes.
0.1.14 (2017-08-08)
-------------------
* Fix alembic script AGAIN
0.1.13 (2017-08-08)
-------------------
* Don't kill tempmon client if DB session.commit() fails
* Grow the ``Reading.degrees_f`` column
* Fix tempmon alembic script per continuum needs
0.1.12 (2017-08-04)
-------------------
* Auto-delete child objects when deleting Client or Probe object
0.1.11 (2017-07-07)
-------------------
* Switch license to GPL v3 (no longer Affero)
0.1.10 (2017-07-06)
-------------------
* Add ``rattail purge-tempmon`` command
* Tweak import placement to fix startup
0.1.9 (2017-06-01)
------------------
* Fix bug when marking client as offline from server loop
0.1.8 (2017-06-01)
------------------
* Refactor main server loop a bit, to add basic retry w/ error logging
* Tweak mail templates a bit, to reference config values
0.1.7 (2017-06-01)
------------------
* Add ``rattail export-hotcooler`` command, for initial hotcooler support
* Add client error logging in case committing session fails..
0.1.6 (2017-02-09)
------------------
* Add configurable delay per client; improve client try/catch
0.1.5 (2016-12-12)
------------------
* Add config for "good temp" email
0.1.4 (2016-12-11)
------------------
* Hopefully fix alert logic when status becomes good
0.1.3 (2016-12-10)
------------------
* Add email config for tempmon-server alerts
* Add mail templates to project manifest
0.1.2 (2016-12-10)
------------------
* Add support for dummy probes (random temp data)
* Add mail templates, plus initial status alert delay for probes
0.1.1 (2016-12-05)
------------------
* Fix import bug in server daemon
0.1.0 (2016-12-05)
------------------
* Initial release.

40
fabfile.py vendored Normal file
View file

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 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 Affero 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 Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Fabric script for 'rattail-tempmon' package
"""
from __future__ import unicode_literals, absolute_import
import shutil
from fabric.api import task, local
@task
def release():
"""
Release a new version of `rattail-tempmon`
"""
shutil.rmtree('rattail_tempmon.egg-info')
local('python setup.py sdist --formats=gztar upload')

View file

@ -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

View file

@ -2,22 +2,22 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
# Copyright © 2010-2016 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""

View file

@ -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__ = u'0.1.7'

View file

@ -1,36 +1,37 @@
# -*- coding: utf-8; -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# Copyright © 2010-2016 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
TempMon client daemon
"""
from __future__ import unicode_literals, absolute_import
import time
import datetime
import random
import socket
import logging
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm.exc import NoResultFound
from rattail.daemon import Daemon
@ -56,7 +57,6 @@ class TempmonClient(Daemon):
# figure out which client we are
hostname = self.config.get('tempmon.client', 'hostname', default=socket.gethostname())
log.info("i think my hostname is: %s", hostname)
session = Session()
try:
client = session.query(tempmon.Client)\
@ -70,92 +70,47 @@ class TempmonClient(Daemon):
session.close()
# main loop: take readings, pause, repeat
self.failed_checks = 0
while True:
self.take_readings(client_uuid)
time.sleep(self.delay)
def take_readings(self, client_uuid):
"""
Take new readings for all enabled probes on this client.
"""
# log.debug("taking readings")
session = Session()
session = Session()
try:
client = session.get(tempmon.Client, client_uuid)
self.delay = client.delay or 60
if client.enabled:
for probe in client.enabled_probes():
self.take_reading(session, probe)
session.flush()
try:
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)
# one more thing, make sure our client appears "online"
except:
log.exception("Failed to read/record temperature data (but will keep trying)")
session.rollback()
else:
# make sure we show as being online
if not client.online:
client.online = True
try:
session.commit()
except:
log.exception("failed to commit changes to database")
raise
except Exception as error:
log_error = True
self.failed_checks += 1
session.rollback()
finally:
session.close()
# our goal here is to suppress logging when we see connection
# errors which are due to a simple postgres restart. but if they
# keep coming then we'll go ahead and log them (sending email)
if isinstance(error, OperationalError):
# this first test works upon first DB restart, as well as the
# first time after DB stop. but in the case of DB stop,
# subsequent errors will instead match the second test
if error.connection_invalidated or (
'could not connect to server: Connection refused' in str(error)):
# only suppress logging for 3 failures, after that we let them go
# TODO: should make the max attempts configurable
if self.failed_checks < 4:
log_error = False
log.debug("database connection failure #%s: %s",
self.failed_checks,
str(error))
# send error email unless we're suppressing it for now
if log_error:
log.exception("Failed to read/record temperature data (but will keep trying)")
else: # taking readings was successful
self.failed_checks = 0
session.commit()
finally:
session.close()
time.sleep(self.delay)
def take_reading(self, session, probe):
"""
Take a single reading and add to Rattail database.
"""
reading = tempmon.Reading()
try:
reading.degrees_f = self.read_temp(probe)
except:
log.exception("Failed to read temperature (but will keep trying) for probe: %s", probe)
else:
# a reading of 185.0 °F indicates some sort of power issue. when this
# happens we log an error (which sends basic email) but do not record
# the temperature. that way the server doesn't see the 185.0 reading
# and send out a "false alarm" about the temperature being too high.
# https://www.controlbyweb.com/support/faq/temp-sensor-reading-error.html
if reading.degrees_f == 185.0:
log.error("got reading of 185.0 from probe: %s", probe.description)
else: # we have a good reading
reading.client = probe.client
reading.probe = probe
reading.taken = datetime.datetime.utcnow()
session.add(reading)
return reading
reading.client = probe.client
reading.probe = probe
reading.degrees_f = self.read_temp(probe)
reading.taken = datetime.datetime.utcnow()
session.add(reading)
return reading
def read_temp(self, probe):
"""
@ -170,9 +125,7 @@ class TempmonClient(Daemon):
equals_pos = lines[1].find('t=')
if equals_pos != -1:
temp_string = lines[1][equals_pos+2:]
# temperature data comes in as celsius
temp_c = float(temp_string) / 1000.0
# convert celsius to fahrenheit
temp_f = temp_c * 9.0 / 5.0 + 32.0
return round(temp_f,4)
@ -184,18 +137,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)

View file

@ -2,179 +2,97 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar
# 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tempmon commands
"""
import datetime
import logging
from enum import Enum
from pathlib import Path
from __future__ import unicode_literals, absolute_import
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
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,
):
"""
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)
@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')
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
parser.add_argument('-p', '--pidfile',
help="Path to PID file.", metavar='PATH')
parser.add_argument('-D', '--daemonize', action='store_true',
help="Daemonize when starting.")
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.server import make_daemon
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)
daemon = make_daemon(self.config, args.pidfile)
if args.subcommand == 'start':
daemon.start(args.daemonize)
elif args.subcommand == 'stop':
daemon.stop()

View file

@ -1,36 +1,37 @@
# -*- coding: utf-8; -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar
# Copyright © 2010-2016 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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')

View file

@ -2,22 +2,22 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
# Copyright © 2010-2016 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""

View file

@ -6,10 +6,9 @@ Alembic Environment Script
from __future__ import unicode_literals, absolute_import
from alembic import context
from sqlalchemy import orm
from rattail.config import make_config
from rattail.db.config import configure_versioning
from rattail_tempmon.db import model as tempmon
# this is the Alembic Config object, which provides
@ -17,12 +16,7 @@ from rattail.db.config import configure_versioning
alembic_config = context.config
# Use same config file for Rattail, as we are for Alembic.
rattail_config = make_config(alembic_config.config_file_name, usedb=False)
# configure Continuum...this is trickier than I'd like
configure_versioning(rattail_config)
from rattail_tempmon.db import model as tempmon
orm.configure_mappers()
rattail_config = make_config(alembic_config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8; mode: python -*-
# -*- coding: utf-8; -*-
# -*- coding: utf-8 -*-
"""${message}
Revision ID: ${up_revision}

View file

@ -1,39 +0,0 @@
# -*- coding: utf-8; -*-
"""add client, probe notes
Revision ID: 34041e1032a2
Revises: e9eb7fc0a451
Create Date: 2018-09-28 12:24:11.348627
"""
from __future__ import unicode_literals, absolute_import
# revision identifiers, used by Alembic.
revision = '34041e1032a2'
down_revision = u'e9eb7fc0a451'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# client
op.add_column('client', sa.Column('notes', sa.Text(), nullable=True))
# probe
op.add_column('probe', sa.Column('notes', sa.Text(), nullable=True))
def downgrade():
# probe
op.drop_column('probe', 'notes')
# client
op.drop_column('client', 'notes')

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8; -*-
"""add client.disk_type
Revision ID: 5f2b87474433
Revises: 34041e1032a2
Create Date: 2018-09-28 19:08:02.743343
"""
from __future__ import unicode_literals, absolute_import
# revision identifiers, used by Alembic.
revision = '5f2b87474433'
down_revision = u'34041e1032a2'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# client
op.add_column('client', sa.Column('disk_type', sa.Integer(), nullable=True))
def downgrade():
# client
op.drop_column('client', 'disk_type')

View file

@ -1,33 +0,0 @@
# -*- coding: utf-8; -*-
"""grow degrees
Revision ID: 75c09e11543c
Revises: 76d52bb064b8
Create Date: 2017-08-05 12:50:50.093173
"""
from __future__ import unicode_literals, absolute_import
# revision identifiers, used by Alembic.
revision = '75c09e11543c'
down_revision = u'76d52bb064b8'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# reading
op.alter_column('reading', 'degrees_f', type_=sa.Numeric(precision=8, scale=4))
def downgrade():
# reading
op.alter_column('reading', 'degrees_f', type_=sa.Numeric(precision=7, scale=4))

View file

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

View file

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

View file

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

View file

@ -1,36 +0,0 @@
# -*- coding: utf-8; -*-
"""add client.archived
Revision ID: e9eb7fc0a451
Revises: 75c09e11543c
Create Date: 2018-09-28 12:12:19.813042
"""
from __future__ import unicode_literals, absolute_import
# revision identifiers, used by Alembic.
revision = 'e9eb7fc0a451'
down_revision = u'75c09e11543c'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# client
op.add_column('client', sa.Column('archived', sa.Boolean(), nullable=True))
client = sa.sql.table('client', sa.sql.column('archived'))
op.execute(client.update().values({'archived': False}))
op.alter_column('client', 'archived', nullable=False)
def downgrade():
# client
op.drop_column('client', 'archived')

View file

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

View file

@ -1,39 +1,38 @@
# -*- coding: utf-8; -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 Lance Edgar
# 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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.
@ -94,41 +55,13 @@ class Client(Base):
hostname = sa.Column(sa.String(length=255), nullable=False)
location = sa.Column(sa.String(length=255), nullable=True)
disk_type = sa.Column(sa.Integer(), nullable=True, doc="""
Integer code representing the type of hard disk used, if known. The
original motivation for this was to keep track of whether each client
(aka. Raspberry Pi) was using a SD card or USB drive for the root disk.
""")
delay = sa.Column(sa.Integer(), nullable=True, doc="""
Number of seconds to delay between reading / recording temperatures. If
not set, a default of 60 seconds will be assumed.
""")
notes = sa.Column(sa.Text(), nullable=True, doc="""
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.
""")
online = sa.Column(sa.Boolean(), nullable=False, default=False, doc="""
Whether the client is known to be online currently. When a client takes
readings, it will mark itself as online. If the server notices that the
readings have stopped, it will mark the client as *not* online.
""")
archived = sa.Column(sa.Boolean(), nullable=False, default=False, doc="""
Flag indicating this client is "archived". This typically means that the
client itself no longer exists but that the configuration for it should be
retained, to be used as a reference later etc. Note that "archiving" a
client is different from "disabling" it; i.e. disabling is temporary and
archiving is more for the long term.
""")
enabled = sa.Column(sa.Boolean(), nullable=False, default=False)
online = sa.Column(sa.Boolean(), nullable=False, default=False)
def __str__(self):
return '{} ({})'.format(self.config_key, self.hostname)
@ -137,6 +70,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 +78,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'),
)
@ -158,141 +91,22 @@ class Probe(Base):
""",
backref=orm.backref(
'probes',
cascade='all, delete-orphan',
doc="""
List of probes connected to this client.
"""))
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.
""")
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.
""")
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.
""")
notes = sa.Column(sa.Text(), nullable=True, doc="""
Any arbitrary notes for the probe.
""")
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)
therm_status_timeout = sa.Column(sa.Integer(), nullable=False)
status_alert_timeout = sa.Column(sa.Integer(), nullable=False)
status = sa.Column(sa.Integer(), nullable=True)
status_changed = sa.Column(sa.DateTime(), nullable=True)
@ -301,101 +115,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.
@ -413,24 +134,17 @@ class Reading(Base):
Client,
doc="""
Reference to the tempmon client which took this reading.
""",
backref=orm.backref(
'readings',
cascade='all, delete-orphan'))
""")
probe_uuid = sa.Column(sa.String(length=32), nullable=False)
probe = orm.relationship(
Probe,
doc="""
Reference to the tempmon probe which took this reading.
""",
backref=orm.backref(
'readings',
order_by='Reading.taken',
cascade='all, delete-orphan'))
""")
taken = sa.Column(sa.DateTime(), nullable=False, default=datetime.datetime.utcnow)
degrees_f = sa.Column(sa.Numeric(precision=8, scale=4), nullable=False)
degrees_f = sa.Column(sa.Numeric(precision=7, scale=4), nullable=False)
def __str__(self):
return str(self.degrees_f)

View file

@ -1,23 +1,23 @@
# -*- coding: utf-8; -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar
# Copyright © 2010-2016 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
@ -30,60 +30,40 @@ from rattail.db import model
from rattail.mail import Email
from rattail.time import localtime
from rattail_tempmon.db import model as tempmon
class TempmonBase(object):
class tempmon(object):
"""
Generic base class for all tempmon-related emails; adds common sample data.
"""
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': [],
}
class tempmon_critical_high_temp(TempmonBase, Email):
class tempmon_critical_temp(tempmon, 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]
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_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]
return data
class tempmon_error(TempmonBase, Email):
class tempmon_error(tempmon, Email):
"""
Sent when a tempmon probe is noticed to have some error, i.e. no current readings.
"""
@ -96,19 +76,12 @@ class tempmon_error(TempmonBase, Email):
return data
class tempmon_client_offline(TempmonBase, Email):
"""
Sent when a tempmon client has been marked offline.
"""
default_subject = "Client Offline"
class tempmon_good_temp(TempmonBase, Email):
class tempmon_good_temp(tempmon, 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)
@ -116,12 +89,12 @@ class tempmon_good_temp(TempmonBase, Email):
return data
class tempmon_high_temp(TempmonBase, Email):
class tempmon_high_temp(tempmon, 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)
@ -129,32 +102,14 @@ class tempmon_high_temp(TempmonBase, Email):
return data
class tempmon_low_temp(TempmonBase, Email):
class tempmon_low_temp(tempmon, 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)
data['status'] = self.enum.TEMPMON_PROBE_STATUS[self.enum.TEMPMON_PROBE_STATUS_LOW_TEMP]
return data
class tempmon_disabled_probes(Email):
"""
Notifies of any Tempmon client devices or probes which are disabled.
"""
default_subject = "Disabled probes"
def sample_data(self, request):
return {
'disabled_clients': [
tempmon.Client(config_key='foo', hostname='foo.example.com'),
],
'disabled_probes': [
tempmon.Probe(description="north wall of walk-in cooler",
client=tempmon.Client(config_key='bar')),
],
}

View file

@ -1,53 +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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tempmon data problems
"""
from __future__ import unicode_literals, absolute_import
from rattail.mail import send_email
from rattail_tempmon.db import Session as TempmonSession, model as tempmon
def disabled_probes(config, progress=None):
"""
Notifies if any (non-archived) Tempmon client devices or probes are disabled.
"""
tempmon_session = TempmonSession()
clients = tempmon_session.query(tempmon.Client)\
.filter(tempmon.Client.archived == 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 != None)\
.filter(tempmon.Probe.enabled == None)\
.all()
if clients or probes:
send_email(config, 'tempmon_disabled_probes', {
'disabled_clients': clients,
'disabled_probes': probes,
})
tempmon_session.close()

View file

@ -1,37 +1,35 @@
# -*- coding: utf-8; -*-
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar
# Copyright © 2010-2016 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.
# terms of the GNU Affero 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.
# FOR A PARTICULAR PURPOSE. See the GNU Affero 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/>.
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tempmon server daemon
"""
from __future__ import unicode_literals, absolute_import
import time
import datetime
import logging
import humanize
from sqlalchemy import orm
from sqlalchemy.exc import OperationalError
from rattail.db import Session, api
from rattail_tempmon.db import Session as TempmonSession, model as tempmon
from rattail.daemon import Daemon
@ -53,148 +51,57 @@ class TempmonServerDaemon(Daemon):
Keeps an eye on tempmon readings and sends alerts as needed.
"""
self.extra_emails = self.config.getlist('rattail.tempmon', 'extra_emails', default=[])
delay = self.config.getint('rattail.tempmon', 'server.delay', default=60)
self.failed_checks = 0
while True:
self.check_readings()
time.sleep(delay)
# TODO: make this configurable
time.sleep(60)
def check_readings(self):
# log.debug("checking readings")
self.now = make_utc()
session = TempmonSession()
try:
clients = session.query(tempmon.Client)\
.filter(tempmon.Client.enabled != None)\
.filter(tempmon.Client.archived == False)
for client in clients:
self.check_readings_for_client(session, client)
session.flush()
clients = session.query(tempmon.Client)\
.filter(tempmon.Client.enabled == True)
for client in clients:
self.check_readings_for_client(session, client)
except Exception as error:
log_error = True
self.failed_checks += 1
session.rollback()
session.commit()
session.close()
# our goal here is to suppress logging when we see connection
# errors which are due to a simple postgres restart. but if they
# keep coming then we'll go ahead and log them (sending email)
if isinstance(error, OperationalError):
# this first test works upon first DB restart, as well as the
# first time after DB stop. but in the case of DB stop,
# subsequent errors will instead match the second test
if error.connection_invalidated or (
'could not connect to server: Connection refused' in str(error)):
# only suppress logging for 3 failures, after that we let them go
# TODO: should make the max attempts configurable
if self.failed_checks < 4:
log_error = False
log.debug("database connection failure #%s: %s",
self.failed_checks,
str(error))
# send error email unless we're suppressing it for now
if log_error:
log.exception("Failed to check client probe readings (but will keep trying)")
else: # checks were successful
self.failed_checks = 0
session.commit()
finally:
session.close()
# TODO: not sure this is really necessary?
self.set_last_checked()
def check_readings_for_client(self, session, client):
"""
Check readings for all (enabled) probes for the given client.
"""
# cutoff is calculated as the client delay (i.e. how often it takes
# readings) plus one minute. we "should" have a reading for each probe
# within that time window. if no readings are found we will consider
# 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):
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
# mark it offline
if client.online and not online:
# we consider client offline if it has failed to take readings for
# 3 times in a row. allow a one minute buffer for good measure.
cutoff = self.now - datetime.timedelta(seconds=(delay * 3) + 60)
reading = session.query(tempmon.Reading)\
.filter(tempmon.Reading.client == client)\
.filter(tempmon.Reading.taken >= cutoff)\
.first()
if not reading:
log.info("marking client as OFFLINE: {}".format(client))
client.online = False
send_email(self.config, 'tempmon_client_offline', {
'client': client,
'now': localtime(self.config, self.now, from_utc=True),
})
self.check_readings_for_probe(session, probe, cutoff)
def check_readings_for_probe(self, session, probe, cutoff):
"""
Check readings for the given probe, within the time window defined by
the given cutoff.
"""
# we really only care about the latest reading
reading = session.query(tempmon.Reading)\
.filter(tempmon.Reading.probe == probe)\
.filter(tempmon.Reading.taken >= cutoff)\
.order_by(tempmon.Reading.taken.desc())\
.first()
if reading:
readings = session.query(tempmon.Reading)\
.filter(tempmon.Reading.probe == probe)\
.filter(tempmon.Reading.taken >= cutoff)\
.all()
if readings:
# we really only care about the latest reading
reading = sorted(readings, key=lambda r: r.taken)[-1]
# 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)
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:
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)
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)
return True
else: # no current readings for probe
else: # no readings for probe
self.update_status(probe, self.enum.TEMPMON_PROBE_STATUS_ERROR)
return False
def update_status(self, probe, status, reading=None):
data = {
@ -202,32 +109,20 @@ class TempmonServerDaemon(Daemon):
'status': self.enum.TEMPMON_PROBE_STATUS[status],
'reading': reading,
'taken': localtime(self.config, reading.taken, from_utc=True) if reading else None,
'now': localtime(self.config, self.now, from_utc=True),
'now': localtime(self.config),
}
prev_status = probe.status
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,69 +135,30 @@ 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:
timeout = datetime.timedelta(minutes=probe.therm_status_timeout)
if (self.now - probe.status_changed) <= 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_HIGH_TEMP : 'tempmon_high_temp',
self.enum.TEMPMON_PROBE_STATUS_CRITICAL_TEMP : 'tempmon_critical_temp',
self.enum.TEMPMON_PROBE_STATUS_ERROR : 'tempmon_error',
}
self.send_email(status, msgtypes[status], data)
send_email(self.config, msgtypes[status], data)
# maybe send more emails if config said so
for msgtype in self.extra_emails:
self.send_email(status, msgtype, data)
send_email(self.config, 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
# 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)
data['status_since'] = since.strftime('%I:%M %p')
data['status_since_delta'] = humanize.naturaltime(self.now - started)
# fetch last 90 minutes of readings
session = orm.object_session(probe)
recent_minutes = 90 # TODO: make configurable
cutoff = self.now - datetime.timedelta(seconds=(60 * recent_minutes))
readings = session.query(tempmon.Reading)\
.filter(tempmon.Reading.probe == probe)\
.filter(tempmon.Reading.taken >= cutoff)\
.order_by(tempmon.Reading.taken.desc())
data['recent_minutes'] = recent_minutes
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)
def set_last_checked(self):
session = Session()
api.save_setting(session, 'tempmon.server.last_checked', self.now.strftime(self.timefmt))
session.commit()
session.close()
def make_daemon(config, pidfile=None):

View file

@ -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 <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

View file

@ -1,8 +0,0 @@
## -*- coding: utf-8 -*-
<html>
<body>
<p>At ${now.strftime("%Y-%m-%d %I:%M %p")} the Tempmon server failed to locate readings for ${client}.<br>
This client is now marked as offline. Investigate asap.
</p>
</body>
</html>

View file

@ -1,25 +0,0 @@
## -*- coding: utf-8 -*-
<html>
<body>
<p>
<b>This is an alert from ${probe}!</b><br>
The status of ${probe} is: ${status}.<br>
The current temperature is: ${reading.degrees_f}.<br>
The temperature should never be this high.
Investigate Immediately!<br>
</p>
<p>
Notes: <br>
Frozen food that is above 40 degrees needs to be thrown away<br>
if it remains at that temperature for two hours or more.<br>
</p>
<p>
Check out <a href="http://www.fsis.usda.gov/wps/portal/fsis/topics/food-safety-education/get-answers/food-safety-fact-sheets/safe-food-handling/freezing-and-food-safety/CT_Index/!ut/p/a1/jZFRT8IwEIB_DY9dbw7J8G1ZYtiUTYJK2Qsp7NYt2dqlrU759RZ8UQJK-9LefV-ud6UFZbSQ_L0R3DZK8vZwLyYbWMDEn8aQ5lP_HpLsdZE_xDGEy1sHrP8AsuBK_8KK4D8_vaLAjZ7Hc0GLntuaNLJSlAm0hEszoDaUVUqVxPAK7Sep-M4SUyNalzjEyDFbc1m2jRQO1oh7d3J6SX6YlMXPm0SW-EFXtPj9KvDdTrJgOZ6lWQD5-BQ4M7Zv4PJcXOOiVdvjH60juQ1C16HGCjVq7027cG1tb-5GMIJhGDyhlGjR26nunFArYyk74fruhe0foxk0T90qNNEXiOIqAA!!/#16">this USDA link</a> for useful information
</p>
<p>
This email will repeat every ${probe.status_alert_timeout} minutes until the issue<br>
has been resolved.
</p>
<p>
</body>
</html>

View file

@ -17,7 +17,7 @@
Check out <a href="http://www.fsis.usda.gov/wps/portal/fsis/topics/food-safety-education/get-answers/food-safety-fact-sheets/safe-food-handling/freezing-and-food-safety/CT_Index/!ut/p/a1/jZFRT8IwEIB_DY9dbw7J8G1ZYtiUTYJK2Qsp7NYt2dqlrU759RZ8UQJK-9LefV-ud6UFZbSQ_L0R3DZK8vZwLyYbWMDEn8aQ5lP_HpLsdZE_xDGEy1sHrP8AsuBK_8KK4D8_vaLAjZ7Hc0GLntuaNLJSlAm0hEszoDaUVUqVxPAK7Sep-M4SUyNalzjEyDFbc1m2jRQO1oh7d3J6SX6YlMXPm0SW-EFXtPj9KvDdTrJgOZ6lWQD5-BQ4M7Zv4PJcXOOiVdvjH60juQ1C16HGCjVq7027cG1tb-5GMIJhGDyhlGjR26nunFArYyk74fruhe0foxk0T90qNNEXiOIqAA!!/#16">this USDA link</a> for useful information
</p>
<p>
This email will repeat every ${probe.status_alert_timeout} minutes until the issue<br>
This email will repeat every 15 minutes until the issue<br>
has been resolved.
</p>
<p>

View file

@ -1,31 +0,0 @@
## -*- coding: utf-8; -*-
<html>
<body>
<h1>Disabled Tempmon Probes</h1>
<p>
We checked if there were any offline temperature probes at your location.&nbsp;
Someone wanted these deactivated at some point, but you should make sure it is
okay that they still are.&nbsp; Here are the results:
</p>
% if disabled_probes:
<h3>Disabled Probes</h3>
<ul>
% for probe in disabled_probes:
<li>${probe} (for client: ${probe.client.config_key})</li>
% endfor
</ul>
% endif
% if disabled_clients:
<h3>Disabled Clients</h3>
<ul>
% for client in disabled_clients:
<li>${client}</li>
% endfor
</ul>
% endif
<p>Please contact the Tech Department with any questions.</p>
</body>
</html>

View file

@ -4,7 +4,7 @@
<p>At ${taken or now}, ${probe} reported that its status was: ${status}.<br>
Something went wrong. Please investigate as soon as possible.
</p>
<p>This email will repeat every ${probe.status_alert_timeout} minutes until the issue is resolved.
<p>This email will repeat every 15 minutes until the issue is resolved.
</p>
</body>
</html>

View file

@ -10,7 +10,7 @@
</p>
<p>
Notes:<br>
This alert will happen every ${probe.status_alert_timeout} minutes until<br>
This alert will happen every 15 minutes until<br>
the temperature reaches an acceptable level.<br>
</p>
</body>

View file

@ -10,7 +10,7 @@
</p>
<p>
Notes:<br>
This alert will happen every ${probe.status_alert_timeout} minutes until<br>
This alert will happen every 15 minutes until<br>
the temperature reaches an acceptable level.<br>
</p>
</body>

109
setup.py Normal file
View file

@ -0,0 +1,109 @@
# -*- 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 Affero 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 Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero 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__))
execfile(os.path.join(here, 'rattail_tempmon', '_version.py'))
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 Affero 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 Affero General Public License v3',
'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',
'tempmon-client = rattail_tempmon.commands:TempmonClient',
'tempmon-server = rattail_tempmon.commands:TempmonServer',
],
'rattail.config.extensions': [
'tempmon = rattail_tempmon.config:TempmonConfigExtension',
],
},
)

View file

@ -1,43 +0,0 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2024 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/>.
#
################################################################################
"""
Tasks for 'rattail-tempmon' package
"""
import os
import shutil
from invoke import task
@task
def release(c):
"""
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/*')