mirror of https://github.com/lumapu/ahoy.git
committed by
GitHub
18 changed files with 784 additions and 391 deletions
@ -1,4 +1,4 @@ |
|||
#ifndef __STYLE_CSS_H__ |
|||
#define __STYLE_CSS_H__ |
|||
const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}"; |
|||
const char style_css[] PROGMEM = "h1 {margin:0;padding:20pt;font-size:22pt;color:#fff;background-color:#006ec0;display:block;text-transform:uppercase;}html, body {font-family:Arial;margin:0;padding:0;}p {text-align:justify;font-size:13pt;}.des {margin-top:35px;font-size:13pt;color:#006ec0;}.subdes {font-size:12pt;color:#006ec0;margin-left:7px;}a:link, a:visited {text-decoration:none;font-size:13pt;color:#006ec0;}a:hover, a:focus {color:#f00;}a.erase {background-color:#006ec0;color:#fff;padding:7px;display:inline-block;margin-top:30px;float:right;}#content {padding:15px 15px 60px 15px;}#footer {position:fixed;bottom:0px;height:45px;background-color:#006ec0;width:100%;border-top:5px solid #fff;}#footer p, #footer a {color:#fff;padding:0 7px 0 7px;font-size:10pt !important;}div.content {background-color:#fff;padding-bottom:65px;overflow:auto;}input, select {padding:7px;font-size:13pt;}input.text, select {width:70%;box-sizing:border-box;margin-bottom:10px;border:1px solid #ccc;}input.btn {background-color:#006ec0;color:#fff;border:0px;float:right;margin:10px 0 30px;text-transform:uppercase;}input.cb {margin-bottom:20px;}label {width:20%;display:inline-block;font-size:12pt;padding-right:10px;margin-left:10px;}.left {float:left;}.right {float:right;}div.ch-iv {width:100%;background-color:#32b004;display:inline-block;margin-bottom:20px;padding-bottom:20px;overflow:auto;}div.ch {width:250px;min-height:420px;background-color:#006ec0;display:inline-block;margin-right:20px;margin-bottom:20px;overflow:auto;padding-bottom:20px;}div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head {color:#fff;display:block;width:100%;text-align:center;}.subgrp {float:left;width:250px;}div.ch .unit, div.ch-iv .unit {font-size:19px;margin-left:10px;}div.ch .value, div.ch-iv .value {margin-top:20px;font-size:30px;}div.ch .info, div.ch-iv .info {margin-top:3px;font-size:10px;}div.ch .head {background-color:#003c80;padding:10px 0 10px 0;}div.ch-iv .head {background-color:#1c6800;padding:10px 0 10px 0;}div.iv {max-width:1060px;}div.ch:last-child {margin-right:0px !important;}#note {margin:50px 10px 10px 10px;padding-top:10px;width:100%;border-top:1px solid #bbb;}@media(max-width:500px) {div.ch .unit, div.ch-iv .unit {font-size:18px;}div.ch {width:170px;min-height:100px;}.subgrp {width:180px;}}"; |
|||
#endif /*__STYLE_CSS_H__*/ |
|||
|
@ -0,0 +1,197 @@ |
|||
#!/usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
""" |
|||
Hoymiles output plugin library |
|||
""" |
|||
|
|||
import socket |
|||
from datetime import datetime, timezone |
|||
from hoymiles.decoders import StatusResponse |
|||
|
|||
try: |
|||
from influxdb_client import InfluxDBClient |
|||
except ModuleNotFoundError: |
|||
pass |
|||
|
|||
class OutputPluginFactory: |
|||
def __init__(self, **params): |
|||
""" |
|||
Initialize output plugin |
|||
|
|||
:param inverter_ser: The inverter serial |
|||
:type inverter_ser: str |
|||
:param inverter_name: The configured name for the inverter |
|||
:type inverter_name: str |
|||
""" |
|||
|
|||
self.inverter_ser = params.get('inverter_ser', '') |
|||
self.inverter_name = params.get('inverter_name', None) |
|||
|
|||
def store_status(self, response, **params): |
|||
""" |
|||
Default function |
|||
|
|||
:raises NotImplementedError: when the plugin does not implement store status data |
|||
""" |
|||
raise NotImplementedError('The current output plugin does not implement store_status') |
|||
|
|||
class InfluxOutputPlugin(OutputPluginFactory): |
|||
""" Influx2 output plugin """ |
|||
api = None |
|||
|
|||
def __init__(self, url, token, **params): |
|||
""" |
|||
Initialize InfluxOutputPlugin |
|||
|
|||
The following targets must be present in your InfluxDB. This does not |
|||
automatically create anything for You. |
|||
|
|||
:param str url: The url to connect this client to. Like http://localhost:8086 |
|||
:param str token: Influx2 access token which is allowed to write to bucket |
|||
:param org: Influx2 org, the token belongs to |
|||
:type org: str |
|||
:param bucket: Influx2 bucket to store data in (also known as retention policy) |
|||
:type bucket: str |
|||
:param measurement: Default measurement-prefix to use |
|||
:type measurement: str |
|||
""" |
|||
super().__init__(**params) |
|||
|
|||
self._bucket = params.get('bucket', 'hoymiles/autogen') |
|||
self._org = params.get('org', '') |
|||
self._measurement = params.get('measurement', |
|||
f'inverter,host={socket.gethostname()}') |
|||
|
|||
client = InfluxDBClient(url, token, bucket=self._bucket) |
|||
self.api = client.write_api() |
|||
|
|||
def store_status(self, response, **params): |
|||
""" |
|||
Publish StatusResponse object |
|||
|
|||
:param hoymiles.decoders.StatusResponse response: StatusResponse object |
|||
:type response: hoymiles.decoders.StatusResponse |
|||
:param measurement: Custom influx measurement name |
|||
:type measurement: str or None |
|||
|
|||
:raises ValueError: when response is not instance of StatusResponse |
|||
""" |
|||
|
|||
if not isinstance(response, StatusResponse): |
|||
raise ValueError('Data needs to be instance of StatusResponse') |
|||
|
|||
data = response.__dict__() |
|||
|
|||
measurement = self._measurement + f',location={data["inverter_ser"]}' |
|||
|
|||
data_stack = [] |
|||
|
|||
time_rx = datetime.now() |
|||
if 'time' in data and isinstance(data['time'], datetime): |
|||
time_rx = data['time'] |
|||
|
|||
# InfluxDB uses UTC |
|||
utctime = datetime.fromtimestamp(time_rx.timestamp(), tz=timezone.utc) |
|||
|
|||
# InfluxDB requires nanoseconds |
|||
ctime = int(utctime.timestamp() * 1e9) |
|||
|
|||
# AC Data |
|||
phase_id = 0 |
|||
for phase in data['phases']: |
|||
data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}') |
|||
data_stack.append(f'{measurement},phase={phase_id},type=voltage value={phase["voltage"]} {ctime}') |
|||
data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') |
|||
phase_id = phase_id + 1 |
|||
|
|||
# DC Data |
|||
string_id = 0 |
|||
for string in data['strings']: |
|||
data_stack.append(f'{measurement},string={string_id},type=total value={string["energy_total"]/1000:.4f} {ctime}') |
|||
data_stack.append(f'{measurement},string={string_id},type=power value={string["power"]:.2f} {ctime}') |
|||
data_stack.append(f'{measurement},string={string_id},type=voltage value={string["voltage"]:.3f} {ctime}') |
|||
data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {ctime}') |
|||
string_id = string_id + 1 |
|||
# Global |
|||
data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') |
|||
data_stack.append(f'{measurement},type=temperature value={data["temperature"]:.2f} {ctime}') |
|||
|
|||
self.api.write(self._bucket, self._org, data_stack) |
|||
|
|||
try: |
|||
import paho.mqtt.client |
|||
except ModuleNotFoundError: |
|||
pass |
|||
|
|||
class MqttOutputPlugin(OutputPluginFactory): |
|||
""" Mqtt output plugin """ |
|||
client = None |
|||
|
|||
def __init__(self, *args, **params): |
|||
""" |
|||
Initialize MqttOutputPlugin |
|||
|
|||
:param host: Broker ip or hostname (defaults to: 127.0.0.1) |
|||
:type host: str |
|||
:param port: Broker port |
|||
:type port: int (defaults to: 1883) |
|||
:param user: Optional username to login to the broker |
|||
:type user: str or None |
|||
:param password: Optional passwort to login to the broker |
|||
:type password: str or None |
|||
:param topic: Topic prefix to use (defaults to: hoymiles/{inverter_ser}) |
|||
:type topic: str |
|||
|
|||
:param paho.mqtt.client.Client broker: mqtt-client instance |
|||
:param str inverter_ser: inverter serial |
|||
:param hoymiles.StatusResponse data: decoded inverter StatusResponse |
|||
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) |
|||
:type topic: str |
|||
""" |
|||
super().__init__(*args, **params) |
|||
|
|||
mqtt_client = paho.mqtt.client.Client() |
|||
mqtt_client.username_pw_set(params.get('user', None), params.get('password', None)) |
|||
mqtt_client.connect(params.get('host', '127.0.0.1'), params.get('port', 1883)) |
|||
mqtt_client.loop_start() |
|||
|
|||
self.client = mqtt_client |
|||
|
|||
def store_status(self, response, **params): |
|||
""" |
|||
Publish StatusResponse object |
|||
|
|||
:param hoymiles.decoders.StatusResponse response: StatusResponse object |
|||
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) |
|||
:type topic: str |
|||
|
|||
:raises ValueError: when response is not instance of StatusResponse |
|||
""" |
|||
|
|||
if not isinstance(response, StatusResponse): |
|||
raise ValueError('Data needs to be instance of StatusResponse') |
|||
|
|||
data = response.__dict__() |
|||
|
|||
topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}') |
|||
|
|||
# AC Data |
|||
phase_id = 0 |
|||
for phase in data['phases']: |
|||
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) |
|||
self.client.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) |
|||
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) |
|||
phase_id = phase_id + 1 |
|||
|
|||
# DC Data |
|||
string_id = 0 |
|||
for string in data['strings']: |
|||
self.client.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000) |
|||
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power']) |
|||
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) |
|||
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) |
|||
string_id = string_id + 1 |
|||
# Global |
|||
self.client.publish(f'{topic}/frequency', data['frequency']) |
|||
self.client.publish(f'{topic}/temperature', data['temperature']) |
@ -0,0 +1 @@ |
|||
influxdb-client>=1.28.0 |
Loading…
Reference in new issue