Browse Source

Merge branch 'main' into development03

pull/729/head
lumapu 2 years ago
parent
commit
dab6b17200
  1. 73
      tools/rpi/README.md
  2. 6
      tools/rpi/ahoy.service
  3. 13
      tools/rpi/ahoy.yml.example
  4. 41
      tools/rpi/hoymiles/__init__.py
  5. 62
      tools/rpi/hoymiles/__main__.py
  6. 52
      tools/rpi/hoymiles/decoders/__init__.py
  7. 76
      tools/rpi/hoymiles/outputs.py

73
tools/rpi/README.md

@ -80,12 +80,79 @@ python3 getting_started.py # to test and see whether RF24 class can be loaded as
If there are no error messages on the last step, then the NRF24 Wrapper has been installed successfully.
Building RF24 Wrapper for Debian 11 (bullseye) 64 bit operating system
----------------------------------------------------------------------
The description above does not work on Debian 11 (bullseye) 64 bit operating system.
Please check first, if you have Debian 11 (bullseye) 64 bit operating system installed:
- `uname -a` search for aarch64
- `lsb_release -d`
- `cat /etc/debian_version`
There are 2 possible solutions to install the RF24 wrapper:
**__1. Solution:__**
```code
sudo apt install cmake git python3-dev libboost-python-dev python3-pip python3-rpi.gpio
sudo ln -s $(ls /usr/lib/$(ls /usr/lib/gcc | \
head -1)/libboost_python3*.so | \
tail -1) /usr/lib/$(ls /usr/lib/gcc | \
head -1)/libboost_python3.so
git clone https://github.com/nRF24/RF24.git
cd RF24
rm -rf build Makefile.inc
./configure --driver=SPIDEV
```
> _edit `Makefile.inc` with your prefered editor e.g. nano or vi_
>
> old:
>```code
> CPUFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard
> CFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard -Ofast -Wall -pthread
>```
> new:
>```code
> CPUFLAGS=
> CFLAGS=-Ofast -Wall -pthread
>```
_continue now_
```code
make
sudo make install
cd pyRF24
rm -r ./build/ ./dist/ ./RF24.egg-info/ ./__pycache__/ #just to make sure there is no old stuff
python3 -m pip install --upgrade pip
python3 -m pip install .
python3 -m pip list #watch for RF24 module - if its there its installed
```
**__2. Solution:__**
```code
sudo apt install git python3-dev libboost-python-dev python3-pip python3-rpi.gpio
git clone --recurse-submodules https://github.com/nRF24/pyRF24.git
cd pyRF24
python3 -m pip install . -v # this step takes about 5 minutes on my RPI-4 !
```
If you have problems with your radio module from ahoi, e.g.: cannot interpret received data,
please try to reduce the speed of your radio module!
Add the following parameter to your ahoy.yml configuration file in "nrf" section:
`spispeed: 600000` (0.6 MHz)
Required python modules
-----------------------
Some modules are not installed by default on a RaspberryPi, therefore add them manually:
```
```code
pip install crcmod pyyaml paho-mqtt SunTimes
```
@ -112,7 +179,7 @@ Python parameters
The application describes itself
```
```code
python3 -m hoymiles --help
usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose]
@ -180,7 +247,7 @@ Todo
- Ability to talk to multiple inverters
- MQTT gateway
- understand channel hopping
- configurable polling interval
- ~~configurable polling interval~~ done: interval ist configurable in ahoy.yml
- commands
- picture of setup!
- python module

6
tools/rpi/ahoy.service

@ -6,11 +6,9 @@
# WorkingDirectory (absolute path to your private ahoy dir)
# To change other config parameter, please consult systemd documentation
#
# To activate this service, create a link, enable and start the ahoy.service
# $ mkdir -p $HOME/.config/systemd/user
# $ ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user
# To activate this service, enable and start ahoy.service
# $ systemctl --user enable $(pwd)/ahoy/tools/rpi/ahoy.service
# $ systemctl --user status ahoy
# $ systemctl --user enable ahoy
# $ systemctl --user start ahoy
# $ systemctl --user status ahoy
#

13
tools/rpi/ahoy.yml.example

@ -31,7 +31,7 @@ ahoy:
QoS: 0
Retain: True
last_will:
topic: Appelweg_PV/114181807700 # defaults to 'hoymiles/{serial}'
topic: my_DTU_name # Name of DTU - default: hoymiles/{DTU-serial}
payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!"
# Influx2 output
@ -96,6 +96,7 @@ ahoy:
dtu:
serial: 99978563001
name: my_DTU_name
inverters:
- name: 'balkon'
@ -103,14 +104,14 @@ ahoy:
txpower: 'low' # txpower per inverter (min,low,high,max)
mqtt:
send_raw_enabled: false # allow inject debug data via mqtt
topic: 'hoymiles/114172221234' # defaults to '{inverter-name}/{serial}'
topic: 'hoymiles/114172220003' # defaults to '{inverter-name}/{serial}'
strings: # list all available strings
- s_name: 'String 1 left' # String 1 name
s_maxpower: 395 # String 1 max power in Wp
s_maxpower: 395 # String 1 max power in inverter
- s_name: 'String 2 right' # String 2 name
s_maxpower: 400 # String 2 max power in Wp
s_maxpower: 400 # String 2 max power in inverter
- s_name: 'String 3 up' # String 3 name
s_maxpower: 405 # String 3 max power in Wp
s_maxpower: 405 # String 3 max power in inverter
- s_name: 'String 4 down' # String 4 name
s_maxpower: 410 # String 4 max power in Wp
s_maxpower: 410 # String 4 max power in inverter

41
tools/rpi/hoymiles/__init__.py

@ -11,8 +11,28 @@ import re
from datetime import datetime
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 *
from os import environ
try:
# OSI Layer 2 driver for nRF24L01 on Arduino & Raspberry Pi/Linux Devices
# https://github.com/nRF24/RF24.git
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
if environ.get('TERM') is not None:
print('Using python Module: RF24')
except ModuleNotFoundError as e:
if environ.get('TERM') is not None:
print(f'{e} - try to use module: RF24')
try:
# Repo for pyRF24 package
# https://github.com/nRF24/pyRF24.git
from pyrf24 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
if environ.get('TERM') is not None:
print(f'{e} - Using python Module: pyrf24')
except ModuleNotFoundError as e:
if environ.get('TERM') is not None:
print(f'{e} - exit')
exit()
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
@ -158,15 +178,26 @@ class ResponseDecoder(ResponseDecoderFactory):
model = self.inverter_model
command = self.request_command
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}')
if HOYMILES_DEBUG_LOGGING:
if command.upper() == '01':
model_desc = "Firmware version / date"
elif command.upper() == '02':
model_desc = "Inverter generic events log"
elif command.upper() == '0B':
model_desc = "mirco-inverters status data"
elif command.upper() == '0C':
model_desc = "mirco-inverters status data"
elif command.upper() == '11':
model_desc = "Inverter generic events log"
elif command.upper() == '12':
model_desc = "Inverter major events log"
logging.info(f'model_decoder: {model}Decode{command.upper()} - {model_desc}')
model_decoders = __import__('hoymiles.decoders')
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
device = getattr(model_decoders, f'{model}Decode{command.upper()}')
else:
if HOYMILES_DEBUG_LOGGING:
device = getattr(model_decoders, 'DebugDecodeAny')
device = getattr(model_decoders, 'DebugDecodeAny')
return device(self.response,
time_rx=self.time_rx,

62
tools/rpi/hoymiles/__main__.py

@ -33,6 +33,12 @@ def signal_handler(sig_num, frame):
if mqtt_client:
mqtt_client.disco()
if influx_client:
influx_client.disco()
if volkszaehler_client:
volkszaehler_client.disco()
sys.exit(0)
signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C)
@ -75,7 +81,6 @@ class SunsetHandler:
else:
logging.info('Sunset disabled.')
def checkWaitForSunrise(self):
if not self.suntimes:
return
@ -94,6 +99,23 @@ class SunsetHandler:
time.sleep(time_to_sleep)
logging.info (f'Woke up...')
def sun_status2mqtt(self, dtu_ser, dtu_name):
if not mqtt_client:
return
local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_zone = self.suntimes.setlocal(datetime.now()).tzinfo._key
if self.suntimes:
mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \
{'dis_night_comm' : 'True', \
'local_sunrise' : local_sunrise, \
'local_sunset' : local_sunset,
'local_zone' : local_zone})
else:
mqtt_client.sun_info2mqtt({'sun_topic': f'{dtu_name}/{dtu_ser}'}, \
{'dis_night_comm': 'False'})
def main_loop(ahoy_config):
"""Main loop"""
inverters = [
@ -101,7 +123,9 @@ 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')
dtu_ser = ahoy_config.get('dtu', {}).get('serial', None)
dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu')
sunset.sun_status2mqtt(dtu_ser, dtu_name)
loop_interval = ahoy_config.get('interval', 1)
try:
@ -112,6 +136,11 @@ def main_loop(ahoy_config):
t_loop_start = time.time()
for inverter in inverters:
if not 'name' in inverter:
inverter['name'] = 'hoymiles'
if not 'serial' in inverter:
logging.error("No inverter serial number found in ahoy.yml - exit")
sys.exit(999)
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3)
@ -122,8 +151,6 @@ def main_loop(ahoy_config):
if time_to_sleep > 0:
time.sleep(time_to_sleep)
except KeyboardInterrupt:
sys.exit()
except Exception as e:
logging.fatal('Exception catched: %s' % e)
logging.fatal(traceback.print_exc())
@ -174,13 +201,14 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
response = com.get_payload()
payload_ttl = 0
except Exception as e_all:
logging.error(f'Error while retrieving data: {e_all}')
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
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:
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
c_datetime = datetime.now()
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
# prepare decoder object
@ -195,7 +223,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
# get decoder object
result = decoder.decode()
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'{c_datetime} Decoded: {result.__dict__()}')
logging.info(f'Decoded: {result.__dict__()}')
# check decoder object for output
if isinstance(result, hoymiles.decoders.StatusResponse):
@ -281,7 +309,13 @@ def init_logging(ahoy_config):
lvl = logging.WARNING
elif level == 'ERROR':
lvl = logging.ERROR
elif level == 'FATAL':
lvl = logging.FATAL
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
lvl = logging.DEBUG
logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl)
dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu')
logging.info(f'start logging for {dtu_name} with level: {logging.root.level}')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles")
@ -308,29 +342,29 @@ if __name__ == '__main__':
logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}')
sys.exit(1)
# read AHOY configuration file and prepare logging
ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config)
if global_config.log_transactions:
hoymiles.HOYMILES_TRANSACTION_LOGGING=True
if global_config.verbose:
hoymiles.HOYMILES_DEBUG_LOGGING=True
# read AHOY configuration file and prepare logging
ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config)
# Prepare for multiple transceivers, makes them configurable
for radio_config in ahoy_config.get('nrf', [{}]):
hmradio = hoymiles.HoymilesNRF(**radio_config)
# create MQTT - client object
mqtt_client = None
mqtt_config = ahoy_config.get('mqtt', {})
mqtt_config = ahoy_config.get('mqtt', None)
if mqtt_config and not mqtt_config.get('disabled', False):
from .outputs import MqttOutputPlugin
mqtt_client = MqttOutputPlugin(mqtt_config)
# create INFLUX - client object
influx_client = None
influx_config = ahoy_config.get('influxdb', {})
influx_config = ahoy_config.get('influxdb', None)
if influx_config and not influx_config.get('disabled', False):
from .outputs import InfluxOutputPlugin
influx_client = InfluxOutputPlugin(

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

@ -99,6 +99,7 @@ class StatusResponse(Response):
frequency = None
powerfactor = None
event_count = None
unpack_error = False
def unpack(self, fmt, base):
"""
@ -110,6 +111,10 @@ class StatusResponse(Response):
:rtype: tuple
"""
size = struct.calcsize(fmt)
if (len(self.response) < base+size):
self.unpack_error = True
logging.error(f'base: {base} size: {size} len: {len(self.response)} fmt: {fmt} rep: {self.response}')
return [0]
return struct.unpack(fmt, self.response[base:base+size])
@property
@ -150,6 +155,7 @@ class StatusResponse(Response):
s_exists = False
string_id = len(strings)
string = {}
string['name'] = self.inv_strings[string_id]['s_name']
for key in self.string_keys:
prop = f'dc_{key}_{string_id}'
if hasattr(self, prop):
@ -193,7 +199,8 @@ class StatusResponse(Response):
data['event_count'] = self.event_count
data['time'] = self.time_rx
return data
if not self.unpack_error:
return data
class UnknownResponse(Response):
"""
@ -321,9 +328,9 @@ class EventsResponse(UnknownResponse):
#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')
logging.info (f' Inverter status: {a_text} ({status})')
self.status = struct.unpack('>H', self.response[:2])[0]
self.a_text = self.alarm_codes.get(self.status, 'N/A')
logging.info (f'Inverter status: {self.a_text} ({self.status})')
chunk_size = 12
for i_chunk in range(2, len(self.response), chunk_size):
@ -331,9 +338,12 @@ class EventsResponse(UnknownResponse):
logging.debug(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
if (len(chunk[0:6]) < 6):
logging.error(f'length of chunk must be greater or equal 6 bytes: {chunk}')
return
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
a_text = self.alarm_codes.get(a_code, 'N/A')
logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
dbg = ''
@ -341,6 +351,14 @@ class EventsResponse(UnknownResponse):
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
logging.debug(dbg)
def __dict__(self):
""" Base values, availabe in each __dict__ call """
data = super().__dict__()
data['inv_stat_num'] = self.status
data['inv_stat_txt'] = self.a_text
return data
class HardwareInfoResponse(UnknownResponse):
def __init__(self, *args, **params):
super().__init__(*args, **params)
@ -361,9 +379,14 @@ class HardwareInfoResponse(UnknownResponse):
def __dict__(self):
""" Base values, availabe in each __dict__ call """
responce_info = self.response
logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}')
data = super().__dict__()
if (len(self.response) != 16):
logging.error(f'HardwareInfoResponse: data length should be 16 bytes - measured {len(self.response)} bytes')
logging.error(f'HardwareInfoResponse: data: {self.response}')
return data
logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", self.response[0:16])}')
fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10])
fw_version_maj = int((fw_version / 10000))
@ -377,7 +400,6 @@ class HardwareInfoResponse(UnknownResponse):
f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\
f'HW revision {hw_id}')
data = super().__dict__()
data['FW_ver_maj'] = fw_version_maj
data['FW_ver_min'] = fw_version_min
data['FW_ver_pat'] = fw_version_pat
@ -468,6 +490,8 @@ class Hm300Decode0B(StatusResponse):
""" String 1 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[0]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property
@ -540,6 +564,8 @@ class Hm600Decode0B(StatusResponse):
""" String 1 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[0]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property
@ -567,6 +593,8 @@ class Hm600Decode0B(StatusResponse):
""" String 2 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[1]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
@property
@ -647,6 +675,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 1 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[0]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property
@ -674,6 +704,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 2 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[1]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
@property
@ -701,6 +733,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 3 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[2]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3)
@property
@ -728,6 +762,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 4 irratiation in percent """
if self.inv_strings is None:
return None
if self.inv_strings[3]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3)
@property

76
tools/rpi/hoymiles/outputs.py

@ -9,6 +9,7 @@ import socket
import logging
from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse, HardwareInfoResponse
from hoymiles import HOYMILES_TRANSACTION_LOGGING, HOYMILES_DEBUG_LOGGING
class OutputPluginFactory:
def __init__(self, **params):
@ -39,6 +40,7 @@ class InfluxOutputPlugin(OutputPluginFactory):
def __init__(self, url, token, **params):
"""
Initialize InfluxOutputPlugin
https://influxdb-client.readthedocs.io/en/stable/api.html#influxdbclient
The following targets must be present in your InfluxDB. This does not
automatically create anything for You.
@ -68,8 +70,12 @@ class InfluxOutputPlugin(OutputPluginFactory):
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()
with InfluxDBClient(url, token, bucket=self._bucket) as self.client:
self.api = self.client.write_api()
def disco(self, **params):
self.client.close() # Shutdown the client
return
def store_status(self, response, **params):
"""
@ -102,6 +108,9 @@ class InfluxOutputPlugin(OutputPluginFactory):
# InfluxDB requires nanoseconds
ctime = int(utctime.timestamp() * 1e9)
if HOYMILES_DEBUG_LOGGING:
logging.info(f'InfluxDB: utctime: {utctime}')
# AC Data
phase_id = 0
for phase in data['phases']:
@ -135,6 +144,9 @@ class InfluxOutputPlugin(OutputPluginFactory):
data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}')
data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}')
if HOYMILES_DEBUG_LOGGING:
#logging.debug(f'INFLUX data to DB: {data_stack}')
pass
self.api.write(self._bucket, self._org, data_stack)
class MqttOutputPlugin(OutputPluginFactory):
@ -196,6 +208,12 @@ class MqttOutputPlugin(OutputPluginFactory):
def disco(self, **params):
self.client.loop_stop() # Stop loop
self.client.disconnect() # disconnect
return
def info2mqtt(self, mqtt_topic, mqtt_data):
for mqtt_key in mqtt_data:
self.client.publish(f'{mqtt_topic["topic"]}/{mqtt_key}', mqtt_data[mqtt_key], self.qos, self.ret)
return
def store_status(self, response, **params):
"""
@ -209,13 +227,18 @@ class MqttOutputPlugin(OutputPluginFactory):
"""
data = response.__dict__()
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
topic = params.get('topic', None)
if not topic:
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
if HOYMILES_DEBUG_LOGGING:
logging.info(f'MQTT-topic: {topic} data-type: {type(response)}')
if isinstance(response, StatusResponse):
# Global Head
if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"), self.qos, self.ret)
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%YT%H:%M:%S"), self.qos, self.ret)
# AC Data
phase_id = 0
@ -233,12 +256,16 @@ class MqttOutputPlugin(OutputPluginFactory):
string_id = 0
string_sum_power = 0
for string in data['strings']:
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000, self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'], self.qos, self.ret)
if 'name' in string:
string_name = string['name'].replace(" ","_")
else:
string_name = string_id
self.client.publish(f'{topic}/emeter-dc/{string_name}/voltage', string['voltage'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/current', string['current'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/power', string['power'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldDay', string['energy_daily'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldTotal', string['energy_total']/1000, self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/Irradiation', string['irradiation'], self.qos, self.ret)
string_id = string_id + 1
string_sum_power += string['power']
@ -277,9 +304,10 @@ class VzInverterOutput:
self.channels = dict()
for channel in config.get('channels', []):
uid = channel.get('uid')
uid = channel.get('uid', None)
ctype = channel.get('type')
if uid and ctype:
# if uid and ctype:
if ctype:
self.channels[ctype] = uid
def store_status(self, data, session):
@ -295,6 +323,9 @@ class VzInverterOutput:
ts = int(round(data['time'].timestamp() * 1000))
if HOYMILES_DEBUG_LOGGING:
logging.info(f'Volkszaehler-Timestamp: {ts}')
# AC Data
phase_id = 0
for phase in data['phases']:
@ -327,13 +358,24 @@ class VzInverterOutput:
if data['yield_today'] is not None:
self.try_publish(ts, f'yield_today', data['yield_today'])
self.try_publish(ts, f'efficiency', data['efficiency'])
return
def try_publish(self, ts, ctype, value):
if not ctype in self.channels:
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
if HOYMILES_DEBUG_LOGGING:
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
return
uid = self.channels[ctype]
url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}'
if uid == None:
if HOYMILES_DEBUG_LOGGING:
logging.debug(f'ctype \"{ctype}\" has no configured uid-value in ahoy.yml')
return
if HOYMILES_DEBUG_LOGGING:
logging.debug(f'VZ-url: {url}')
try:
r = self.session.get(url)
if r.status_code == 404:
@ -344,6 +386,7 @@ class VzInverterOutput:
raise ValueError(f'Transmit result {url}')
except ConnectionError as e:
raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}')
return
class VolkszaehlerOutputPlugin(OutputPluginFactory):
def __init__(self, config, **params):
@ -364,13 +407,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
exit(1)
self.session = requests.Session()
self.inverters = dict()
self.inverters = dict()
for inverterconfig in config.get('inverters', []):
serial = inverterconfig.get('serial')
output = VzInverterOutput(inverterconfig, self.session)
self.inverters[serial] = output
def disco(self, **params):
self.session.close() # closing the connection
return
def store_status(self, response, **params):
"""
Publish StatusResponse object
@ -395,3 +442,4 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
output.store_status(data, self.session)
except ValueError as e:
logging.warning('Could not send data to volkszaehler instance: %s' % e)
return

Loading…
Cancel
Save