4 Commits

Author SHA1 Message Date
Hauke Petersen
3005dbe7e7 verbump to 0.0.4 2022-09-02 11:16:41 +02:00
Hauke Petersen
09a116984c verbump to 0.0.3 2022-09-02 10:19:11 +02:00
Hauke Petersen
1563945858 verbump to 0.0.2 2022-08-26 14:49:40 +02:00
Hauke Petersen
57b97eaf10 steckie initial port to pip based service 2022-08-26 14:49:20 +02:00
6 changed files with 217 additions and 139 deletions

45
setup.py Normal file
View 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.4"
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']
}
)

View File

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

@@ -0,0 +1 @@
from .steckie import Steckie

35
steckie/__main__.py Normal file
View 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()

130
steckie/steckie.py Executable file
View File

@@ -0,0 +1,130 @@
#!/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.request
from datetime import datetime
from deichapp import Deichapp
from deichflux import Deichflux
from apscheduler.schedulers.blocking import BlockingScheduler
import dateutil
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') 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:
self.cnt["year"] = total
self.cnt["month"] = total
self.cnt["day"] = total
elif now_local.date().month != last_local.date().month:
self.cnt["month"] = total
self.cnt["day"] = total
elif now_local.date().day != last_local.date().day:
self.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)

6
test.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
echo "### INSTALL\n"
pip3 install --user .
echo "\n### RUN\n"
steckie config.herrstat.yml