Compare commits
6 Commits
e2f8447f0e
...
v0.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a22b344028 | ||
|
|
cb3e651983 | ||
|
|
3005dbe7e7 | ||
|
|
09a116984c | ||
|
|
1563945858 | ||
|
|
57b97eaf10 |
45
setup.py
Normal file
45
setup.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Copyright (C) 2022 Hauke Petersen <devel@haukepetersen.de>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
name = "steckie"
|
||||||
|
ver = "0.0.6"
|
||||||
|
desc = 'Read state from Tasmota smart plugs'
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name=name,
|
||||||
|
version=ver,
|
||||||
|
author="Hauke Petersen",
|
||||||
|
author_email="devel@haukepetersen.de",
|
||||||
|
description=desc,
|
||||||
|
url=f'https://git.deichnet.com/hauke/{name}',
|
||||||
|
license='MIT',
|
||||||
|
packages=[name],
|
||||||
|
install_requires=[
|
||||||
|
"apscheduler",
|
||||||
|
"requests",
|
||||||
|
"pydeichlib@git+ssh://git@git.deichnet.com:22223"
|
||||||
|
"/hauke/pydeichlib.git@v0.0.13#egg=pydeichlib-0.0.13",
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [f'{name} = {name}.__main__:main']
|
||||||
|
}
|
||||||
|
)
|
||||||
139
steckie.py
139
steckie.py
@@ -1,139 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright (C) 2019 Hauke Petersen <devel@haukepetersen.de>
|
|
||||||
#
|
|
||||||
# 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
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import yaml
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import urllib.request
|
|
||||||
from influxdb import InfluxDBClient
|
|
||||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
|
||||||
|
|
||||||
|
|
||||||
CONFIGFILE = "/opt/steckie/config.yml"
|
|
||||||
CMD_SENSOR = "cm?cmnd=Status%208"
|
|
||||||
|
|
||||||
CFG_DEFAULTS = {
|
|
||||||
"STECKIE_ITVL": 5,
|
|
||||||
"INFLUXDB_PORT": 8086,
|
|
||||||
}
|
|
||||||
|
|
||||||
CFG_NEEDED = {
|
|
||||||
"INFLUXDB_HOST": "hostname",
|
|
||||||
"INFLUXDB_DB": "database_name",
|
|
||||||
"INFLUXDB_USER": "username",
|
|
||||||
"INFLUXDB_USER_PASSWORD": "SuperSecure",
|
|
||||||
}
|
|
||||||
|
|
||||||
CFG_VAR = {
|
|
||||||
"name": r'STECKIE_DEV_\d+_NAME',
|
|
||||||
"url": r'STECKIE_DEV_\d+_URL',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Steckie:
|
|
||||||
def __init__(self, env_file):
|
|
||||||
self.cfg = dict()
|
|
||||||
env = os.environ
|
|
||||||
|
|
||||||
print("Env file: {}".format(env_file))
|
|
||||||
|
|
||||||
# read configuration
|
|
||||||
if env_file:
|
|
||||||
env = self.read_env(env_file)
|
|
||||||
for val in CFG_DEFAULTS:
|
|
||||||
self.cfg[val] = env[val] if val in env else CFG_DEFAULTS[val]
|
|
||||||
for val in CFG_NEEDED:
|
|
||||||
if val in env:
|
|
||||||
self.cfg[val] = env[val]
|
|
||||||
else:
|
|
||||||
logging.error("unable to read {} fron env".format(val))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if cfgfile:
|
|
||||||
try:
|
|
||||||
with open(cfgfile, 'r', encoding="utf-8") as f:
|
|
||||||
self.cfg.update(yaml.load(f, Loader=yaml.BaseLoader))
|
|
||||||
except:
|
|
||||||
logging.error("unable to read config file '{}'".format(cfgfile))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.scheduler = BlockingScheduler()
|
|
||||||
self.db = InfluxDBClient(host=self.cfg['db']['host'],
|
|
||||||
port=int(self.cfg['db']['port']),
|
|
||||||
database=self.cfg['db']['name'],
|
|
||||||
username=self.cfg['db']['user'],
|
|
||||||
password=self.cfg['db']['pass'])
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.scheduler.add_job(self.update, 'interval',
|
|
||||||
seconds=int(self.cfg['update_itvl']))
|
|
||||||
self.scheduler.start()
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
for dev in self.cfg['devs']:
|
|
||||||
url = "{}/{}".format(dev['url'], CMD_SENSOR)
|
|
||||||
data = {}
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(url) as resp:
|
|
||||||
data = json.loads(resp.read().decode("utf-8"))
|
|
||||||
print(data)
|
|
||||||
except:
|
|
||||||
logging.warning("unable to read data from '{}'".format(dev['name']))
|
|
||||||
continue
|
|
||||||
|
|
||||||
point = [{
|
|
||||||
"measurement": "stromie",
|
|
||||||
"tags": {
|
|
||||||
"name": dev['name'],
|
|
||||||
"url": dev['url'],
|
|
||||||
"type": "tasmota_awp07l",
|
|
||||||
},
|
|
||||||
"time": data['StatusSNS']['Time'],
|
|
||||||
"fields": {
|
|
||||||
"voltage": float(data['StatusSNS']['ENERGY']['Voltage']),
|
|
||||||
"current": float(data['StatusSNS']['ENERGY']['Current']),
|
|
||||||
"pwr": float(data['StatusSNS']['ENERGY']['ApparentPower']),
|
|
||||||
"pwr_r": float(data['StatusSNS']['ENERGY']['ReactivePower']),
|
|
||||||
"factor": float(data['StatusSNS']['ENERGY']['Factor']),
|
|
||||||
"e_daily": float(data['StatusSNS']['ENERGY']['Today']),
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.db.write_points(point)
|
|
||||||
except:
|
|
||||||
logging.warning("{}: unable to write to DB".format(dev['name']))
|
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
|
||||||
logging.basicConfig(format='%(asctime)s %(message)s',
|
|
||||||
level=logging.WARNING)
|
|
||||||
steckie = Steckie(args.env_file)
|
|
||||||
steckie.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
p = argparse.ArgumentParser()
|
|
||||||
p.add_argument("env_file", default=None, nargs="?", help="output dump")
|
|
||||||
args = p.parse_args()
|
|
||||||
main(args)
|
|
||||||
1
steckie/__init__.py
Normal file
1
steckie/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .steckie import Steckie
|
||||||
35
steckie/__main__.py
Normal file
35
steckie/__main__.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Copyright (C) 2022 Hauke Petersen <devel@haukepetersen.de>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from steckie import Steckie
|
||||||
|
|
||||||
|
|
||||||
|
def main(args=None):
|
||||||
|
parser = argparse.ArgumentParser(description="Steckie")
|
||||||
|
parser.add_argument("cfg_file", help="Configuration file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app = Steckie(args.cfg_file)
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
129
steckie/steckie.py
Executable file
129
steckie/steckie.py
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (C) 2019 Hauke Petersen <devel@haukepetersen.de>
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import pytz
|
||||||
|
import logging
|
||||||
|
import dateutil
|
||||||
|
import urllib
|
||||||
|
from datetime import datetime
|
||||||
|
from deichapp import Deichapp
|
||||||
|
from deichflux import Deichflux
|
||||||
|
from apscheduler.schedulers.blocking import BlockingScheduler
|
||||||
|
|
||||||
|
CONFIG_DEFAULT = {
|
||||||
|
"interval": 5.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Steckie(Deichapp):
|
||||||
|
def __init__(self, cfg_file):
|
||||||
|
super().__init__(cfg_file, CONFIG_DEFAULT)
|
||||||
|
|
||||||
|
self.scheduler = BlockingScheduler()
|
||||||
|
self.db = Deichflux(self.cfg["db"])
|
||||||
|
|
||||||
|
logging.warning("Steckie V0.72 - loaded...")
|
||||||
|
|
||||||
|
self.counters = {}
|
||||||
|
for name in self.cfg["devs"]:
|
||||||
|
self.counters[name] = {
|
||||||
|
"last": None,
|
||||||
|
"year": 0.0,
|
||||||
|
"month": 0.0,
|
||||||
|
"day": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# read latest known values from database
|
||||||
|
query = f'SELECT e_total_kwh, e_year_kwh, e_month_kwh, e_day_kwh ' \
|
||||||
|
f'FROM "{self.cfg["measurement"]}" WHERE "name"=\'{name}\' ' \
|
||||||
|
f'ORDER BY DESC LIMIT 1'
|
||||||
|
res = self.db.query(query, None)
|
||||||
|
for p in res.get_points():
|
||||||
|
self.counters[name]["last"] = dateutil.parser.isoparse(p["time"])
|
||||||
|
total = p["e_total_kwh"]
|
||||||
|
self.counters[name]["year"] = total - p["e_year_kwh"]
|
||||||
|
self.counters[name]["month"] = total - p["e_month_kwh"]
|
||||||
|
self.counters[name]["day"] = total - p["e_day_kwh"]
|
||||||
|
logging.warning(f'Found {name}: current total is {total}kWh')
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for name, url in self.cfg["devs"].items():
|
||||||
|
self.scheduler.add_job(self.query,
|
||||||
|
"interval",
|
||||||
|
seconds=int(self.cfg["interval"]),
|
||||||
|
args=(name, url))
|
||||||
|
self.scheduler.start()
|
||||||
|
|
||||||
|
def query(self, name, url):
|
||||||
|
cnt = self.counters[name]
|
||||||
|
now = pytz.UTC.localize(datetime.utcnow())
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f'{url}/cm?cmnd=Status%208',
|
||||||
|
timeout=self.cfg["timeout"]) as raw:
|
||||||
|
resp = json.loads(raw.read().decode("utf-8"))
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f'{name} - no response: {e}')
|
||||||
|
return
|
||||||
|
|
||||||
|
total = float(resp["StatusSNS"]["ENERGY"]["Total"])
|
||||||
|
|
||||||
|
# initialize values if this is the first time we read them
|
||||||
|
if cnt["last"] is None:
|
||||||
|
cnt["last"] = now
|
||||||
|
for f in ("year", "month", "day"):
|
||||||
|
cnt[f] = total
|
||||||
|
|
||||||
|
# reset values if a when new time frame starts
|
||||||
|
now_local = now.astimezone(pytz.timezone("Europe/Berlin"))
|
||||||
|
last_local = cnt["last"].astimezone(pytz.timezone("Europe/Berlin"))
|
||||||
|
if now_local.date().year != last_local.date().year:
|
||||||
|
cnt["year"] = total
|
||||||
|
cnt["month"] = total
|
||||||
|
cnt["day"] = total
|
||||||
|
elif now_local.date().month != last_local.date().month:
|
||||||
|
cnt["month"] = total
|
||||||
|
cnt["day"] = total
|
||||||
|
elif now_local.date().day != last_local.date().day:
|
||||||
|
cnt["day"] = total
|
||||||
|
cnt["last"] = now
|
||||||
|
|
||||||
|
# write data to DB
|
||||||
|
point = {
|
||||||
|
"measurement": self.cfg["measurement"],
|
||||||
|
"tags": {
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
"time": now.isoformat(),
|
||||||
|
"fields": {
|
||||||
|
"power_w": float(resp["StatusSNS"]["ENERGY"]["Power"]),
|
||||||
|
"voltage_v": float(resp["StatusSNS"]["ENERGY"]["Voltage"]),
|
||||||
|
"current_a": float(resp["StatusSNS"]["ENERGY"]["Current"]),
|
||||||
|
"factor": float(resp["StatusSNS"]["ENERGY"]["Factor"]),
|
||||||
|
"e_total_kwh": total,
|
||||||
|
"e_year_kwh": total - cnt["year"],
|
||||||
|
"e_month_kwh": total - cnt["month"],
|
||||||
|
"e_day_kwh": total - cnt["day"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.warning(f'{name} - power:{point["fields"]["power_w"]:.1f}W '
|
||||||
|
f'total:{point["fields"]["e_total_kwh"]}kWh')
|
||||||
|
self.db.write(point)
|
||||||
Reference in New Issue
Block a user