Browse Source

Merge pull request #426 from DanielR92/dev03-sysinfos

Add mqtt sunrise/sunset + setupAp + system.html
pull/430/head
Lukas Pusch 2 years ago
committed by GitHub
parent
commit
2f395af8d9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      .github/ISSUE_TEMPLATE/report-ahoy.md
  2. 33
      .github/ISSUE_TEMPLATE/report.yaml
  3. 10
      src/.vscode/extensions.json
  4. 5
      src/.vscode/settings.json
  5. 2
      src/app.cpp
  6. 17
      src/web/html/api.js
  7. 17
      src/web/html/system.html
  8. 35
      src/web/mqtt.h
  9. 15
      src/web/webApi.cpp
  10. 8
      src/wifi/ahoywifi.cpp
  11. 6
      tools/rpi/ahoy.yml.example
  12. 18
      tools/rpi/hoymiles/__init__.py
  13. 71
      tools/rpi/hoymiles/__main__.py
  14. 61
      tools/rpi/hoymiles/decoders/__init__.py
  15. 3
      tools/rpi/hoymiles/outputs.py

31
.github/ISSUE_TEMPLATE/report-ahoy.md

@ -33,18 +33,29 @@ connected between +3.3V and GND (Pin 1 & 2) of the NRF Module
* [ ] Image of the your wiring attached
### Connection diagram I used:
| nRF24L01+ Pin | ESP8266/32 GPIO |
| nRF24L01+ Pin | ESP8266 GPIO |
| ------------- | -------------- |
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO2 CE D4 |
| Pin 4 CSN | GPIO15 CS D8 |
| Pin 5 SCK | GPIO14 SCLK D5 |
| Pin 6 MOSI | GPIO13 MOSI D7 |
| Pin 7 MISO | GPIO12 MISO D6 |
| Pin 8 IRQ | GPIO0 IRQ D3 |
| nRF24L01+ Pin | ESP32 GPIO |
| ------------- | --------------- |
| Pin 1 GND [] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO_2/_4 CE |
| Pin 4 CSN | GPIO15/_5 CS |
| Pin 5 SCK | GPIO14/18 SCLK |
| Pin 6 MOSI | GPIO13/23 MOSI |
| Pin 7 MISO | GPIO12/19 MISO |
| Pin 8 IRQ | GPIO_0/0 IRQ |
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO4 CE D4 |
| Pin 4 CSN | GPIO5 CS D5 |
| Pin 5 SCK | GPIO18 SCLK D18 |
| Pin 6 MOSI | GPIO23 MOSI D23 |
| Pin 7 MISO | GPIO19 MISO D19 |
| Pin 8 IRQ | GPIO0 IRQ D0 |
Note: [] GND Pin 1 has a square mark on the nRF24L01+ module
Note: [*] GND Pin 1 has a square mark on the nRF24L01+ module
## Software
* [ ] AhoyDTU

33
.github/ISSUE_TEMPLATE/report.yaml

@ -81,18 +81,29 @@ body:
description: Tell us which connection diagram you used?
value: |
## Connection diagram I used:
| nRF24L01+ Pin | ESP8266/32 GPIO |
| nRF24L01+ Pin | ESP8266 GPIO |
| ------------- | -------------- |
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO2 CE D4 |
| Pin 4 CSN | GPIO15 CS D8 |
| Pin 5 SCK | GPIO14 SCLK D5 |
| Pin 6 MOSI | GPIO13 MOSI D7 |
| Pin 7 MISO | GPIO12 MISO D6 |
| Pin 8 IRQ | GPIO0 IRQ D3 |
| nRF24L01+ Pin | ESP32 GPIO |
| ------------- | --------------- |
| Pin 1 GND [] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO_2/_4 CE |
| Pin 4 CSN | GPIO15/_5 CS |
| Pin 5 SCK | GPIO14/18 SCLK |
| Pin 6 MOSI | GPIO13/23 MOSI |
| Pin 7 MISO | GPIO12/19 MISO |
| Pin 8 IRQ | GPIO_0/0 IRQ |
Note: [] GND Pin 1 has a square mark on the nRF24L01+ module
| Pin 1 GND [*] | GND |
| Pin 2 +3.3V | +3.3V |
| Pin 3 CE | GPIO4 CE D4 |
| Pin 4 CSN | GPIO5 CS D5 |
| Pin 5 SCK | GPIO18 SCLK D18 |
| Pin 6 MOSI | GPIO23 MOSI D23 |
| Pin 7 MISO | GPIO19 MISO D19 |
| Pin 8 IRQ | GPIO0 IRQ D0 |
Note: [*] GND Pin 1 has a square mark on the nRF24L01+ module
validations:
required: true
- type: checkboxes

10
src/.vscode/extensions.json

@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

5
src/.vscode/settings.json

@ -21,6 +21,9 @@
// https://clang.llvm.org/docs/ClangFormatStyleOptions.html
"C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 0}",
"files.associations": {
"typeinfo": "cpp"
"typeinfo": "cpp",
"string": "cpp",
"istream": "cpp",
"ostream": "cpp"
},
}

2
src/app.cpp

@ -287,7 +287,7 @@ void app::setupMqtt(void) {
mMqttActive = true;
if(mMqttActive)
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mUtcTimestamp);
mMqtt.setup(this, &mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mUtcTimestamp);
}
//-----------------------------------------------------------------------------

17
src/web/html/api.js

@ -31,6 +31,23 @@ function parseESP(obj) {
document.getElementById("esp_type").innerHTML="Board: " + obj["esp_type"];
}
function parseSysInfo(obj) {
document.getElementById("sdkversion").innerHTML= "SDKv.: " + obj["sdk_version"];
document.getElementById("cpufreq").innerHTML= "CPU MHz: " + obj["cpu_freq"] + "MHz";
document.getElementById("chiprevision").innerHTML= "Rev.: " + obj["chip_revision"];
document.getElementById("chipmodel").innerHTML= "Model: " + obj["chip_model"];
document.getElementById("chipcores").innerHTML= "Core: " + obj["chip_cores"];
document.getElementById("esp_type").innerHTML= "Type: " + obj["esp_type"];
document.getElementById("heap_used").innerHTML= "Used: " + obj["heap_used"];
document.getElementById("heap_total").innerHTML= "Total: " + obj["heap_total"];
}
function changeProgressbar(id, value, max) {
document.getElementById(id).value = value;
document.getElementById(id).max = max;
}
function setHide(id, hide) {
var elm = document.getElementById(id);
if(hide) {

17
src/web/html/system.html

@ -17,7 +17,18 @@
<div id="topnav" class="hide"></div>
</div>
<div id="wrapper">
<div id="content"></div>
<div id="content">
<ul>
<li>Heap:<progress id="heap" max="100" value="70"></progress> <span id="heap_used"></span>(<span id="heap_total"></span>)</li>
<li><span id="sdkversion"></span></li>
<li><span id="cpufreq"></span></li>
<li><span id="chiprevision"></span></li>
<li><span id="chipmodel"></span></li>
<li><span id="chipcores"></span></li>
<li><span id="esp_type"></span></li>
</ul>
<div id="system"></div>
</div>
</div>
<div id="footer">
<div class="left">
@ -39,13 +50,15 @@
function parseSys(obj) {
parseVersion(obj);
parseESP(obj);
changeProgressbar("heap", obj["heap_used"], obj["heap_total"]);
}
function parse(obj) {
if(null != obj) {
parseMenu(obj["menu"]);
parseSys(obj["system"]);
var e = document.getElementById("content");
parseSysInfo(obj["system"]);
var e = document.getElementById("system");
e.innerHTML = obj["html"];
if(null != obj["refresh"]) {

35
src/web/mqtt.h

@ -38,16 +38,17 @@ class mqtt {
~mqtt() { }
void setup(cfgMqtt_t *cfg, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) {
void setup(app *app, cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs) {
DPRINTLN(DBG_VERBOSE, F("mqtt.h:setup"));
mAddressSet = true;
mCfg = cfg;
mDevName = devName;
mSys = sys;
mUtcTimestamp = utcTs;
mApp = app;
mCfg_mqtt = cfg_mqtt;
mDevName = devName;
mSys = sys;
mUtcTimestamp = utcTs;
mClient->setServer(mCfg->broker, mCfg->port);
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port);
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
setCallback(std::bind(&mqtt<HMSYSTEM>::cbMqtt, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
@ -75,7 +76,7 @@ class mqtt {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg"));
if(mAddressSet) {
char top[66];
snprintf(top, 66, "%s/%s", mCfg->topic, topic);
snprintf(top, 66, "%s/%s", mCfg_mqtt->topic, topic);
sendMsg2(top, msg, false);
}
}
@ -173,21 +174,21 @@ class mqtt {
if(strlen(mDevName) > 0) {
// der Server und der Port müssen neu gesetzt werden,
// da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat.
mClient->setServer(mCfg->broker, mCfg->port);
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port);
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte
snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg->topic);
snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg_mqtt->topic);
if((strlen(mCfg->user) > 0) && (strlen(mCfg->pwd) > 0))
resub = mClient->connect(mDevName, mCfg->user, mCfg->pwd, lwt, 0, false, "offline");
if((strlen(mCfg_mqtt->user) > 0) && (strlen(mCfg_mqtt->pwd) > 0))
resub = mClient->connect(mDevName, mCfg_mqtt->user, mCfg_mqtt->pwd, lwt, 0, false, "offline");
else
resub = mClient->connect(mDevName, lwt, 0, false, "offline");
// ein Subscribe ist nur nach einem connect notwendig
if(resub) {
char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte
// ToDo: "/devcontrol/#" is hardcoded
snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg->topic);
snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg_mqtt->topic);
DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic));
mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#"
}
@ -226,6 +227,13 @@ class mqtt {
sendMsg("uptime", val);
sendMsg("wifi_rssi", String(WiFi.RSSI()).c_str());
String sunrise = String(mApp->getSunrise());
String sunset = String(mApp->getSunset());
sendMsg("sunrise", sunrise.c_str());
sendMsg("sunset", sunset.c_str());
while(!mSendList.empty()) {
memset(total, 0, sizeof(float) * 4);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
@ -427,13 +435,14 @@ class mqtt {
DPRINTLN(DBG_INFO, F("app::cbMqtt finished"));
}
app *mApp;
WiFiClient mEspClient;
PubSubClient *mClient;
HMSYSTEM *mSys;
uint32_t *mUtcTimestamp;
bool mAddressSet;
cfgMqtt_t *mCfg;
cfgMqtt_t *mCfg_mqtt;
const char *mDevName;
uint32_t mLastReconnect;
uint32_t mTxCnt;

15
src/web/webApi.cpp

@ -153,6 +153,21 @@ void webApi::getSysInfo(JsonObject obj) {
obj[F("ts_sun_upd")] = mApp->getLatestSunTimestamp();
obj[F("wifi_rssi")] = WiFi.RSSI();
obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0);
obj[F("hostname")] = WiFi.getHostname();
obj[F("sdk_version")] = ESP.getSdkVersion();
obj[F("cpu_freq")] = ESP.getCpuFreqMHz();
obj[F("heap_total")] = ESP.getHeapSize();
obj[F("heap_used")] = ESP.getHeapSize() - ESP.getFreeHeap();
obj[F("sketch_total")] = ESP.getFreeSketchSpace();
obj[F("sketch_used")] = ESP.getSketchSize();
//obj[F("littlefs_total")] = LittleFS.totalBytes();
//obj[F("littlefs_used")] = LittleFS.usedBytes();
obj[F("chip_revision")] = ESP.getChipRevision();
obj[F("chip_model")] = ESP.getChipModel();
obj[F("chip_cores")] = ESP.getChipCores();
#if defined(ESP32)
obj[F("esp_type")] = F("ESP32");
#else

8
src/wifi/ahoywifi.cpp

@ -113,6 +113,12 @@ void ahoywifi::setupAp(const char *ssid, const char *pwd) {
DPRINTLN(DBG_VERBOSE, F("app::setupAp"));
IPAddress apIp(192, 168, 4, 1);
DBGPRINTLN(F("\n---------\nAhoy Info:"));
DBGPRINT(F("Version: "));
DBGPRINTLN(String(VERSION_MAJOR) + F(".") + String(VERSION_MINOR) + F(".") + String(VERSION_PATCH));
DBGPRINT(F("Github Hash: "));
DBGPRINTLN(String(AUTO_GIT_HASH));
DBGPRINT(F("\n---------\nAP MODE\nSSID: "));
DBGPRINTLN(ssid);
DBGPRINT(F("PWD: "));
@ -120,6 +126,8 @@ void ahoywifi::setupAp(const char *ssid, const char *pwd) {
DBGPRINT(F("\nActive for: "));
DBGPRINT(String(WIFI_AP_ACTIVE_TIME));
DBGPRINTLN(F(" seconds"));
DBGPRINTLN("\nIp Address: " + apIp[0] + apIp[1] + apIp[2] + apIp[3]);
DBGPRINTLN(F("\n---------\n"));
WiFi.mode(WIFI_AP);

6
tools/rpi/ahoy.yml.example

@ -2,6 +2,12 @@
ahoy:
interval: 5
logging:
filename: 'hoymiles.log'
# DEBUG, INFO, WARNING, ERROR, FATAL
level: 'INFO'
sunset:
disabled: false
latitude: 51.799118

18
tools/rpi/hoymiles/__init__.py

@ -9,7 +9,7 @@ import struct
import time
import re
from datetime import datetime
import json
import logging
import crcmod
from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
from .decoders import *
@ -51,16 +51,6 @@ def ser_to_esb_addr(inverter_ser):
air_order = ser_to_hm_addr(inverter_ser)[::-1] + b'\x01'
return air_order[::-1]
def print_addr(inverter_ser):
"""
Debug print addresses
:param str inverter_ser: inverter serial
"""
print(f"ser# {inverter_ser} ", end='')
print(f" -> HM {' '.join([f'{byte:02x}' for byte in ser_to_hm_addr(inverter_ser)])}", end='')
print(f" -> ESB {' '.join([f'{byte:02x}' for byte in ser_to_esb_addr(inverter_ser)])}")
class ResponseDecoderFactory:
"""
Prepare payload decoder
@ -584,7 +574,7 @@ class InverterTransaction:
if HOYMILES_TRANSACTION_LOGGING:
c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
print(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
logging.debug(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}')
self.radio.transmit(packet, txpower=self.txpower)
@ -592,14 +582,14 @@ class InverterTransaction:
try:
for response in self.radio.receive():
if HOYMILES_TRANSACTION_LOGGING:
print(response)
logging.debug(response)
self.frame_append(response)
wait = True
except TimeoutError:
pass
except BufferError as e:
print(f'Buffer error {e}')
logging.warning(f'Buffer error {e}')
pass
return wait

71
tools/rpi/hoymiles/__main__.py

@ -19,6 +19,7 @@ import yaml
from yaml.loader import SafeLoader
import paho.mqtt.client
import hoymiles
import logging
class InfoCommands(IntEnum):
InverterDevInform_Simple = 0 # 0x00
@ -48,25 +49,24 @@ class SunsetHandler:
longitude = sunset_config.get('longitude')
altitude = sunset_config.get('altitude')
self.suntimes = SunTimes(longitude=longitude, latitude=latitude, altitude=altitude)
self.nextSunset = self.suntimes.setutc(datetime.now())
print (f'Todays sunset is at {self.nextSunset}')
self.nextSunset = self.suntimes.setutc(datetime.utcnow())
logging.info (f'Todays sunset is at {self.nextSunset} UTC')
def checkWaitForSunrise(self):
if not self.suntimes:
return
# if the sunset already happened for today
now = datetime.now()
now = datetime.utcnow()
if self.nextSunset < now:
# wait until the sun rises tomorrow
tomorrow = now + timedelta(days=1)
nextSunrise = self.suntimes.riseutc(tomorrow)
self.nextSunset = self.suntimes.setutc(tomorrow)
time_to_sleep = (nextSunrise - datetime.now()).total_seconds()
print (f'Waiting for sunrise at {nextSunrise} ({time_to_sleep} seconds)')
time_to_sleep = int((nextSunrise - datetime.now()).total_seconds())
logging.info (f'Waiting for sunrise at {nextSunrise} UTC ({time_to_sleep} seconds)')
if time_to_sleep > 0:
time.sleep(time_to_sleep)
print (f'Woke up... next sunset is at {self.nextSunset}')
return
logging.info (f'Woke up... next sunset is at {self.nextSunset} UTC')
def main_loop(ahoy_config):
"""Main loop"""
@ -75,6 +75,7 @@ def main_loop(ahoy_config):
if not inverter.get('disabled', False)]
sunset = SunsetHandler(ahoy_config.get('sunset'))
dtu_ser = ahoy_config.get('dtu', {}).get('serial')
loop_interval = ahoy_config.get('interval', 1)
try:
@ -86,12 +87,10 @@ def main_loop(ahoy_config):
for inverter in inverters:
if hoymiles.HOYMILES_DEBUG_LOGGING:
print(f'Poll inverter {inverter["serial"]}')
poll_inverter(inverter, do_init)
logging.debug(f'Poll inverter {inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3)
do_init = False
print('', end='', flush=True)
if loop_interval > 0:
time_to_sleep = loop_interval - (time.time() - t_loop_start)
if time_to_sleep > 0:
@ -100,12 +99,12 @@ def main_loop(ahoy_config):
except KeyboardInterrupt:
sys.exit()
except Exception as e:
print ('Exception catched: %s' % e)
traceback.print_exc()
logging.fatal('Exception catched: %s' % e)
logging.fatal(traceback.print_exc())
raise
def poll_inverter(inverter, do_init, retries=4):
def poll_inverter(inverter, dtu_ser, do_init, retries):
"""
Send/Receive command_queue, initiate status poll on inverter
@ -114,7 +113,6 @@ def poll_inverter(inverter, do_init, retries=4):
:type retries: int
"""
inverter_ser = inverter.get('serial')
dtu_ser = ahoy_config.get('dtu', {}).get('serial')
# Queue at least status data request
inv_str = str(inverter_ser)
@ -148,14 +146,14 @@ def poll_inverter(inverter, do_init, retries=4):
response = com.get_payload()
payload_ttl = 0
except Exception as e_all:
print(f'Error while retrieving data: {e_all}')
logging.error(f'Error while retrieving data: {e_all}')
pass
# Handle the response data if any
if response:
c_datetime = datetime.now()
if hoymiles.HOYMILES_DEBUG_LOGGING:
print(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
decoder = hoymiles.ResponseDecoder(response,
request=com.request,
inverter_ser=inverter_ser
@ -165,18 +163,18 @@ def poll_inverter(inverter, do_init, retries=4):
data = result.__dict__()
if hoymiles.HOYMILES_DEBUG_LOGGING:
print(f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}', end='')
dbg = f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}'
if data['powerfactor'] is not None:
print(f', pf={data["powerfactor"]}', end='')
dbg += f', pf={data["powerfactor"]}'
phase_id = 0
for phase in data['phases']:
print(f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}', end='')
dbg += f' phase{phase_id}=voltage:{phase["voltage"]}, current:{phase["current"]}, power:{phase["power"]}, frequency:{data["frequency"]}'
phase_id = phase_id + 1
string_id = 0
for string in data['strings']:
print(f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}', end='')
dbg += f' string{string_id}=voltage:{string["voltage"]}, current:{string["current"]}, power:{string["power"]}, total:{string["energy_total"]/1000}, daily:{string["energy_daily"]}'
string_id = string_id + 1
print()
logging.debug(dbg)
if 'event_count' in data:
if event_message_index[inv_str] < data['event_count']:
@ -257,7 +255,7 @@ def mqtt_on_command(client, userdata, message):
inverter_ser = next(
item[0] for item in mqtt_command_topic_subs if item[1] == message.topic)
except StopIteration:
print('Unexpedtedly received mqtt message for {message.topic}')
logging.warning('Unexpedtedly received mqtt message for {message.topic}')
if inverter_ser:
p_message = message.payload.decode('utf-8').lower()
@ -275,14 +273,31 @@ def mqtt_on_command(client, userdata, message):
command_queue[str(inverter_ser)].append(
hoymiles.frame_payload(payload[1:]))
def init_logging(ahoy_config):
log_config = ahoy_config.get('logging')
fn = 'hoymiles.log'
lvl = logging.ERROR
if log_config:
fn = log_config.get('filename', fn)
level = log_config.get('level', 'ERROR')
if level == 'DEBUG':
lvl = logging.DEBUG
elif level == 'INFO':
lvl = logging.INFO
elif level == 'WARNING':
lvl = logging.WARNING
elif level == 'ERROR':
lvl = logging.ERROR
logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles")
parser.add_argument("-c", "--config-file", nargs="?", required=True,
help="configuration file")
parser.add_argument("--log-transactions", action="store_true", default=False,
help="Enable transaction logging output")
help="Enable transaction logging output (loglevel must be DEBUG)")
parser.add_argument("--verbose", action="store_true", default=False,
help="Enable debug output")
help="Enable detailed debug output (loglevel must be DEBUG)")
global_config = parser.parse_args()
# Load ahoy.yml config file
@ -294,13 +309,14 @@ if __name__ == '__main__':
with open('ahoy.yml', 'r') as fh_yaml:
cfg = yaml.load(fh_yaml, Loader=SafeLoader)
except FileNotFoundError:
print("Could not load config file. Try --help")
logging.error("Could not load config file. Try --help")
sys.exit(2)
except yaml.YAMLError as e_yaml:
print('Failed to load config frile {global_config.config_file}: {e_yaml}')
logging.error('Failed to load config file {global_config.config_file}: {e_yaml}')
sys.exit(1)
ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config)
# Prepare for multiple transceivers, makes them configurable (currently
# only one supported)
@ -367,4 +383,5 @@ if __name__ == '__main__':
mqtt_client.subscribe(topic_item[1])
mqtt_command_topic_subs.append(topic_item)
logging.info(f'Starting main_loop with inverter(s) {g_inverters}')
main_loop(ahoy_config)

61
tools/rpi/hoymiles/decoders/__init__.py

@ -8,6 +8,7 @@ Hoymiles Micro-Inverters decoder library
import struct
from datetime import datetime, timedelta
import crcmod
import logging
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
@ -44,18 +45,20 @@ def print_table_unpack(s_fmt, payload, cw=6):
l_hexlified = [f'{byte:02x}' for byte in payload]
print(f'{"Pos": <{cw}}', end='')
print(''.join([f'{num: >{cw}}' for num in range(0, len(payload))]))
print(f'{"Hex": <{cw}}', end='')
print(''.join([f'{byte: >{cw}}' for byte in l_hexlified]))
dbg = f'{"Pos": <{cw}}'
dbg += ''.join([f'{num: >{cw}}' for num in range(0, len(payload))])
logging.debug(dbg)
dbg = f'{"Hex": <{cw}}'
dbg += ''.join([f'{byte: >{cw}}' for byte in l_hexlified])
logging.debug(dbg)
l_fmt = struct.calcsize(s_fmt)
if len(payload) >= l_fmt:
for offset in range(0, l_fmt):
print(f'{s_fmt: <{cw}}', end='')
print(' ' * cw * offset, end='')
print(''.join(
[f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])]))
dbg = f'{s_fmt: <{cw}}'
dbg += ' ' * cw * offset
dbg += ''.join([f'{num[0]: >{cw*l_fmt}}' for num in g_unpack(s_fmt, payload[offset:])])
logging.debug(dbg)
class Response:
""" All Response Shared methods """
@ -299,27 +302,28 @@ class EventsResponse(UnknownResponse):
crc_valid = self.validate_crc_m()
if crc_valid:
#print(' payload has valid modbus crc')
#logging.debug(' payload has valid modbus crc')
self.response = self.response[:-2]
status = struct.unpack('>H', self.response[:2])[0]
a_text = self.alarm_codes.get(status, 'N/A')
print (f' Inverter status: {a_text} ({status})')
logging.info (f' Inverter status: {a_text} ({status})')
chunk_size = 12
for i_chunk in range(2, len(self.response), chunk_size):
chunk = self.response[i_chunk:i_chunk+chunk_size]
print(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
logging.debug(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
a_text = self.alarm_codes.get(a_code, 'N/A')
print(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
dbg = ''
for fmt in ['BBHHHHH']:
print(f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)))
print(end='', flush=True)
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
logging.debug(dbg)
class HardwareInfoResponse(UnknownResponse):
def __init__(self, *args, **params):
@ -340,8 +344,7 @@ class HardwareInfoResponse(UnknownResponse):
fw_version_pat = int((fw_version % 100))
fw_build_mm = int(fw_build_mmdd / 100)
fw_build_dd = int(fw_build_mmdd % 100)
print()
print(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}')
logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} build at {fw_build_dd}/{fw_build_mm}/{fw_build_yyyy}, HW revision {hw_id}')
class DebugDecodeAny(UnknownResponse):
"""Default decoder"""
@ -351,40 +354,40 @@ class DebugDecodeAny(UnknownResponse):
crc8_valid = self.validate_crc8()
if crc8_valid:
print(' payload has valid crc8')
logging.debug(' payload has valid crc8')
self.response = self.response[:-1]
crc_valid = self.validate_crc_m()
if crc_valid:
print(' payload has valid modbus crc')
logging.debug(' payload has valid modbus crc')
self.response = self.response[:-2]
l_payload = len(self.response)
print(f' payload has {l_payload} bytes')
logging.debug(f' payload has {l_payload} bytes')
print()
print('Field view: int')
logging.debug()
logging.debug('Field view: int')
print_table_unpack('>B', self.response)
print()
print('Field view: shorts')
logging.debug()
logging.debug('Field view: shorts')
print_table_unpack('>H', self.response)
print()
print('Field view: longs')
logging.debug()
logging.debug('Field view: longs')
print_table_unpack('>L', self.response)
try:
if len(self.response) > 2:
print(' type utf-8 : ' + self.response.decode('utf-8'))
logging.debug(' type utf-8 : ' + self.response.decode('utf-8'))
except UnicodeDecodeError:
print(' type utf-8 : utf-8 decode error')
logging.debug(' type utf-8 : utf-8 decode error')
try:
if len(self.response) > 2:
print(' type ascii : ' + self.response.decode('ascii'))
logging.debug(' type ascii : ' + self.response.decode('ascii'))
except UnicodeDecodeError:
print(' type ascii : ascii decode error')
logging.debug(' type ascii : ascii decode error')
# 1121-Series Intervers, 1 MPPT, 1 Phase

3
tools/rpi/hoymiles/outputs.py

@ -6,6 +6,7 @@ Hoymiles output plugin library
"""
import socket
import logging
from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse
@ -310,4 +311,4 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
try:
output.store_status(data, self.session)
except ValueError as e:
print('Could not send data to volkszaehler instance: %s' % e)
logging.warning('Could not send data to volkszaehler instance: %s' % e)

Loading…
Cancel
Save