mirror of
https://github.com/lumapu/ahoy.git
synced 2025-07-26 04:37:14 +02:00
Merge branch 'main' into development03
This commit is contained in:
commit
dab6b17200
7 changed files with 269 additions and 54 deletions
|
@ -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.
|
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
|
Required python modules
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
Some modules are not installed by default on a RaspberryPi, therefore add them manually:
|
Some modules are not installed by default on a RaspberryPi, therefore add them manually:
|
||||||
|
|
||||||
```
|
```code
|
||||||
pip install crcmod pyyaml paho-mqtt SunTimes
|
pip install crcmod pyyaml paho-mqtt SunTimes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -112,7 +179,7 @@ Python parameters
|
||||||
|
|
||||||
|
|
||||||
The application describes itself
|
The application describes itself
|
||||||
```
|
```code
|
||||||
python3 -m hoymiles --help
|
python3 -m hoymiles --help
|
||||||
usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose]
|
usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose]
|
||||||
|
|
||||||
|
@ -180,7 +247,7 @@ Todo
|
||||||
- Ability to talk to multiple inverters
|
- Ability to talk to multiple inverters
|
||||||
- MQTT gateway
|
- MQTT gateway
|
||||||
- understand channel hopping
|
- understand channel hopping
|
||||||
- configurable polling interval
|
- ~~configurable polling interval~~ done: interval ist configurable in ahoy.yml
|
||||||
- commands
|
- commands
|
||||||
- picture of setup!
|
- picture of setup!
|
||||||
- python module
|
- python module
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
# WorkingDirectory (absolute path to your private ahoy dir)
|
# WorkingDirectory (absolute path to your private ahoy dir)
|
||||||
# To change other config parameter, please consult systemd documentation
|
# To change other config parameter, please consult systemd documentation
|
||||||
#
|
#
|
||||||
# To activate this service, create a link, enable and start the ahoy.service
|
# To activate this service, enable and start ahoy.service
|
||||||
# $ mkdir -p $HOME/.config/systemd/user
|
# $ systemctl --user enable $(pwd)/ahoy/tools/rpi/ahoy.service
|
||||||
# $ ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user
|
|
||||||
# $ systemctl --user status ahoy
|
# $ systemctl --user status ahoy
|
||||||
# $ systemctl --user enable ahoy
|
|
||||||
# $ systemctl --user start ahoy
|
# $ systemctl --user start ahoy
|
||||||
# $ systemctl --user status ahoy
|
# $ systemctl --user status ahoy
|
||||||
#
|
#
|
||||||
|
|
|
@ -31,7 +31,7 @@ ahoy:
|
||||||
QoS: 0
|
QoS: 0
|
||||||
Retain: True
|
Retain: True
|
||||||
last_will:
|
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!"
|
payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!"
|
||||||
|
|
||||||
# Influx2 output
|
# Influx2 output
|
||||||
|
@ -96,6 +96,7 @@ ahoy:
|
||||||
|
|
||||||
dtu:
|
dtu:
|
||||||
serial: 99978563001
|
serial: 99978563001
|
||||||
|
name: my_DTU_name
|
||||||
|
|
||||||
inverters:
|
inverters:
|
||||||
- name: 'balkon'
|
- name: 'balkon'
|
||||||
|
@ -103,14 +104,14 @@ ahoy:
|
||||||
txpower: 'low' # txpower per inverter (min,low,high,max)
|
txpower: 'low' # txpower per inverter (min,low,high,max)
|
||||||
mqtt:
|
mqtt:
|
||||||
send_raw_enabled: false # allow inject debug data via 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
|
strings: # list all available strings
|
||||||
- s_name: 'String 1 left' # String 1 name
|
- 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_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_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_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
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,28 @@ import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
import crcmod
|
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 .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_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
|
||||||
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
|
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
|
||||||
|
@ -158,14 +178,25 @@ class ResponseDecoder(ResponseDecoderFactory):
|
||||||
model = self.inverter_model
|
model = self.inverter_model
|
||||||
command = self.request_command
|
command = self.request_command
|
||||||
|
|
||||||
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
|
if HOYMILES_DEBUG_LOGGING:
|
||||||
logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}')
|
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')
|
model_decoders = __import__('hoymiles.decoders')
|
||||||
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
|
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
|
||||||
device = getattr(model_decoders, f'{model}Decode{command.upper()}')
|
device = getattr(model_decoders, f'{model}Decode{command.upper()}')
|
||||||
else:
|
else:
|
||||||
if HOYMILES_DEBUG_LOGGING:
|
|
||||||
device = getattr(model_decoders, 'DebugDecodeAny')
|
device = getattr(model_decoders, 'DebugDecodeAny')
|
||||||
|
|
||||||
return device(self.response,
|
return device(self.response,
|
||||||
|
|
|
@ -33,6 +33,12 @@ def signal_handler(sig_num, frame):
|
||||||
if mqtt_client:
|
if mqtt_client:
|
||||||
mqtt_client.disco()
|
mqtt_client.disco()
|
||||||
|
|
||||||
|
if influx_client:
|
||||||
|
influx_client.disco()
|
||||||
|
|
||||||
|
if volkszaehler_client:
|
||||||
|
volkszaehler_client.disco()
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C)
|
signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C)
|
||||||
|
@ -75,7 +81,6 @@ class SunsetHandler:
|
||||||
else:
|
else:
|
||||||
logging.info('Sunset disabled.')
|
logging.info('Sunset disabled.')
|
||||||
|
|
||||||
|
|
||||||
def checkWaitForSunrise(self):
|
def checkWaitForSunrise(self):
|
||||||
if not self.suntimes:
|
if not self.suntimes:
|
||||||
return
|
return
|
||||||
|
@ -94,6 +99,23 @@ class SunsetHandler:
|
||||||
time.sleep(time_to_sleep)
|
time.sleep(time_to_sleep)
|
||||||
logging.info (f'Woke up...')
|
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):
|
def main_loop(ahoy_config):
|
||||||
"""Main loop"""
|
"""Main loop"""
|
||||||
inverters = [
|
inverters = [
|
||||||
|
@ -101,7 +123,9 @@ def main_loop(ahoy_config):
|
||||||
if not inverter.get('disabled', False)]
|
if not inverter.get('disabled', False)]
|
||||||
|
|
||||||
sunset = SunsetHandler(ahoy_config.get('sunset'))
|
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)
|
loop_interval = ahoy_config.get('interval', 1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -112,6 +136,11 @@ def main_loop(ahoy_config):
|
||||||
t_loop_start = time.time()
|
t_loop_start = time.time()
|
||||||
|
|
||||||
for inverter in inverters:
|
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:
|
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||||
logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
|
logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
|
||||||
poll_inverter(inverter, dtu_ser, do_init, 3)
|
poll_inverter(inverter, dtu_ser, do_init, 3)
|
||||||
|
@ -122,8 +151,6 @@ def main_loop(ahoy_config):
|
||||||
if time_to_sleep > 0:
|
if time_to_sleep > 0:
|
||||||
time.sleep(time_to_sleep)
|
time.sleep(time_to_sleep)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.fatal('Exception catched: %s' % e)
|
logging.fatal('Exception catched: %s' % e)
|
||||||
logging.fatal(traceback.print_exc())
|
logging.fatal(traceback.print_exc())
|
||||||
|
@ -174,13 +201,14 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
|
||||||
response = com.get_payload()
|
response = com.get_payload()
|
||||||
payload_ttl = 0
|
payload_ttl = 0
|
||||||
except Exception as e_all:
|
except Exception as e_all:
|
||||||
|
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
|
||||||
logging.error(f'Error while retrieving data: {e_all}')
|
logging.error(f'Error while retrieving data: {e_all}')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Handle the response data if any
|
# Handle the response data if any
|
||||||
if response:
|
if response:
|
||||||
|
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
|
||||||
c_datetime = datetime.now()
|
c_datetime = datetime.now()
|
||||||
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
|
||||||
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
|
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
|
||||||
|
|
||||||
# prepare decoder object
|
# prepare decoder object
|
||||||
|
@ -195,7 +223,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
|
||||||
# get decoder object
|
# get decoder object
|
||||||
result = decoder.decode()
|
result = decoder.decode()
|
||||||
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
if hoymiles.HOYMILES_DEBUG_LOGGING:
|
||||||
logging.info(f'{c_datetime} Decoded: {result.__dict__()}')
|
logging.info(f'Decoded: {result.__dict__()}')
|
||||||
|
|
||||||
# check decoder object for output
|
# check decoder object for output
|
||||||
if isinstance(result, hoymiles.decoders.StatusResponse):
|
if isinstance(result, hoymiles.decoders.StatusResponse):
|
||||||
|
@ -281,7 +309,13 @@ def init_logging(ahoy_config):
|
||||||
lvl = logging.WARNING
|
lvl = logging.WARNING
|
||||||
elif level == 'ERROR':
|
elif level == 'ERROR':
|
||||||
lvl = logging.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)
|
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__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles")
|
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}')
|
logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}')
|
||||||
sys.exit(1)
|
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:
|
if global_config.log_transactions:
|
||||||
hoymiles.HOYMILES_TRANSACTION_LOGGING=True
|
hoymiles.HOYMILES_TRANSACTION_LOGGING=True
|
||||||
if global_config.verbose:
|
if global_config.verbose:
|
||||||
hoymiles.HOYMILES_DEBUG_LOGGING=True
|
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
|
# Prepare for multiple transceivers, makes them configurable
|
||||||
for radio_config in ahoy_config.get('nrf', [{}]):
|
for radio_config in ahoy_config.get('nrf', [{}]):
|
||||||
hmradio = hoymiles.HoymilesNRF(**radio_config)
|
hmradio = hoymiles.HoymilesNRF(**radio_config)
|
||||||
|
|
||||||
# create MQTT - client object
|
# create MQTT - client object
|
||||||
mqtt_client = None
|
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):
|
if mqtt_config and not mqtt_config.get('disabled', False):
|
||||||
from .outputs import MqttOutputPlugin
|
from .outputs import MqttOutputPlugin
|
||||||
mqtt_client = MqttOutputPlugin(mqtt_config)
|
mqtt_client = MqttOutputPlugin(mqtt_config)
|
||||||
|
|
||||||
# create INFLUX - client object
|
# create INFLUX - client object
|
||||||
influx_client = None
|
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):
|
if influx_config and not influx_config.get('disabled', False):
|
||||||
from .outputs import InfluxOutputPlugin
|
from .outputs import InfluxOutputPlugin
|
||||||
influx_client = InfluxOutputPlugin(
|
influx_client = InfluxOutputPlugin(
|
||||||
|
|
|
@ -99,6 +99,7 @@ class StatusResponse(Response):
|
||||||
frequency = None
|
frequency = None
|
||||||
powerfactor = None
|
powerfactor = None
|
||||||
event_count = None
|
event_count = None
|
||||||
|
unpack_error = False
|
||||||
|
|
||||||
def unpack(self, fmt, base):
|
def unpack(self, fmt, base):
|
||||||
"""
|
"""
|
||||||
|
@ -110,6 +111,10 @@ class StatusResponse(Response):
|
||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
"""
|
"""
|
||||||
size = struct.calcsize(fmt)
|
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])
|
return struct.unpack(fmt, self.response[base:base+size])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -150,6 +155,7 @@ class StatusResponse(Response):
|
||||||
s_exists = False
|
s_exists = False
|
||||||
string_id = len(strings)
|
string_id = len(strings)
|
||||||
string = {}
|
string = {}
|
||||||
|
string['name'] = self.inv_strings[string_id]['s_name']
|
||||||
for key in self.string_keys:
|
for key in self.string_keys:
|
||||||
prop = f'dc_{key}_{string_id}'
|
prop = f'dc_{key}_{string_id}'
|
||||||
if hasattr(self, prop):
|
if hasattr(self, prop):
|
||||||
|
@ -193,6 +199,7 @@ class StatusResponse(Response):
|
||||||
data['event_count'] = self.event_count
|
data['event_count'] = self.event_count
|
||||||
data['time'] = self.time_rx
|
data['time'] = self.time_rx
|
||||||
|
|
||||||
|
if not self.unpack_error:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class UnknownResponse(Response):
|
class UnknownResponse(Response):
|
||||||
|
@ -321,9 +328,9 @@ class EventsResponse(UnknownResponse):
|
||||||
#logging.debug(' payload has valid modbus crc')
|
#logging.debug(' payload has valid modbus crc')
|
||||||
self.response = self.response[:-2]
|
self.response = self.response[:-2]
|
||||||
|
|
||||||
status = struct.unpack('>H', self.response[:2])[0]
|
self.status = struct.unpack('>H', self.response[:2])[0]
|
||||||
a_text = self.alarm_codes.get(status, 'N/A')
|
self.a_text = self.alarm_codes.get(self.status, 'N/A')
|
||||||
logging.info (f' Inverter status: {a_text} ({status})')
|
logging.info (f'Inverter status: {self.a_text} ({self.status})')
|
||||||
|
|
||||||
chunk_size = 12
|
chunk_size = 12
|
||||||
for i_chunk in range(2, len(self.response), chunk_size):
|
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]) + ': ')
|
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])
|
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
|
||||||
a_text = self.alarm_codes.get(a_code, 'N/A')
|
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}')
|
logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
|
||||||
|
|
||||||
dbg = ''
|
dbg = ''
|
||||||
|
@ -341,6 +351,14 @@ class EventsResponse(UnknownResponse):
|
||||||
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
|
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
|
||||||
logging.debug(dbg)
|
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):
|
class HardwareInfoResponse(UnknownResponse):
|
||||||
def __init__(self, *args, **params):
|
def __init__(self, *args, **params):
|
||||||
super().__init__(*args, **params)
|
super().__init__(*args, **params)
|
||||||
|
@ -361,9 +379,14 @@ class HardwareInfoResponse(UnknownResponse):
|
||||||
def __dict__(self):
|
def __dict__(self):
|
||||||
""" Base values, availabe in each __dict__ call """
|
""" Base values, availabe in each __dict__ call """
|
||||||
|
|
||||||
responce_info = self.response
|
data = super().__dict__()
|
||||||
logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}')
|
|
||||||
|
|
||||||
|
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, 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))
|
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'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}')
|
f'HW revision {hw_id}')
|
||||||
|
|
||||||
data = super().__dict__()
|
|
||||||
data['FW_ver_maj'] = fw_version_maj
|
data['FW_ver_maj'] = fw_version_maj
|
||||||
data['FW_ver_min'] = fw_version_min
|
data['FW_ver_min'] = fw_version_min
|
||||||
data['FW_ver_pat'] = fw_version_pat
|
data['FW_ver_pat'] = fw_version_pat
|
||||||
|
@ -468,6 +490,8 @@ class Hm300Decode0B(StatusResponse):
|
||||||
""" String 1 irratiation in percent """
|
""" String 1 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -540,6 +564,8 @@ class Hm600Decode0B(StatusResponse):
|
||||||
""" String 1 irratiation in percent """
|
""" String 1 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -567,6 +593,8 @@ class Hm600Decode0B(StatusResponse):
|
||||||
""" String 2 irratiation in percent """
|
""" String 2 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -647,6 +675,8 @@ class Hm1200Decode0B(StatusResponse):
|
||||||
""" String 1 irratiation in percent """
|
""" String 1 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -674,6 +704,8 @@ class Hm1200Decode0B(StatusResponse):
|
||||||
""" String 2 irratiation in percent """
|
""" String 2 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -701,6 +733,8 @@ class Hm1200Decode0B(StatusResponse):
|
||||||
""" String 3 irratiation in percent """
|
""" String 3 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -728,6 +762,8 @@ class Hm1200Decode0B(StatusResponse):
|
||||||
""" String 4 irratiation in percent """
|
""" String 4 irratiation in percent """
|
||||||
if self.inv_strings is None:
|
if self.inv_strings is None:
|
||||||
return 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)
|
return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -9,6 +9,7 @@ import socket
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from hoymiles.decoders import StatusResponse, HardwareInfoResponse
|
from hoymiles.decoders import StatusResponse, HardwareInfoResponse
|
||||||
|
from hoymiles import HOYMILES_TRANSACTION_LOGGING, HOYMILES_DEBUG_LOGGING
|
||||||
|
|
||||||
class OutputPluginFactory:
|
class OutputPluginFactory:
|
||||||
def __init__(self, **params):
|
def __init__(self, **params):
|
||||||
|
@ -39,6 +40,7 @@ class InfluxOutputPlugin(OutputPluginFactory):
|
||||||
def __init__(self, url, token, **params):
|
def __init__(self, url, token, **params):
|
||||||
"""
|
"""
|
||||||
Initialize InfluxOutputPlugin
|
Initialize InfluxOutputPlugin
|
||||||
|
https://influxdb-client.readthedocs.io/en/stable/api.html#influxdbclient
|
||||||
|
|
||||||
The following targets must be present in your InfluxDB. This does not
|
The following targets must be present in your InfluxDB. This does not
|
||||||
automatically create anything for You.
|
automatically create anything for You.
|
||||||
|
@ -68,8 +70,12 @@ class InfluxOutputPlugin(OutputPluginFactory):
|
||||||
self._org = params.get('org', '')
|
self._org = params.get('org', '')
|
||||||
self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}')
|
self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}')
|
||||||
|
|
||||||
client = InfluxDBClient(url, token, bucket=self._bucket)
|
with InfluxDBClient(url, token, bucket=self._bucket) as self.client:
|
||||||
self.api = client.write_api()
|
self.api = self.client.write_api()
|
||||||
|
|
||||||
|
def disco(self, **params):
|
||||||
|
self.client.close() # Shutdown the client
|
||||||
|
return
|
||||||
|
|
||||||
def store_status(self, response, **params):
|
def store_status(self, response, **params):
|
||||||
"""
|
"""
|
||||||
|
@ -102,6 +108,9 @@ class InfluxOutputPlugin(OutputPluginFactory):
|
||||||
# InfluxDB requires nanoseconds
|
# InfluxDB requires nanoseconds
|
||||||
ctime = int(utctime.timestamp() * 1e9)
|
ctime = int(utctime.timestamp() * 1e9)
|
||||||
|
|
||||||
|
if HOYMILES_DEBUG_LOGGING:
|
||||||
|
logging.info(f'InfluxDB: utctime: {utctime}')
|
||||||
|
|
||||||
# AC Data
|
# AC Data
|
||||||
phase_id = 0
|
phase_id = 0
|
||||||
for phase in data['phases']:
|
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=YieldToday value={data["yield_today"]/1000:.3f} {ctime}')
|
||||||
data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {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)
|
self.api.write(self._bucket, self._org, data_stack)
|
||||||
|
|
||||||
class MqttOutputPlugin(OutputPluginFactory):
|
class MqttOutputPlugin(OutputPluginFactory):
|
||||||
|
@ -196,6 +208,12 @@ class MqttOutputPlugin(OutputPluginFactory):
|
||||||
def disco(self, **params):
|
def disco(self, **params):
|
||||||
self.client.loop_stop() # Stop loop
|
self.client.loop_stop() # Stop loop
|
||||||
self.client.disconnect() # disconnect
|
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):
|
def store_status(self, response, **params):
|
||||||
"""
|
"""
|
||||||
|
@ -209,13 +227,18 @@ class MqttOutputPlugin(OutputPluginFactory):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = response.__dict__()
|
data = response.__dict__()
|
||||||
|
topic = params.get('topic', None)
|
||||||
|
if not topic:
|
||||||
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
|
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):
|
if isinstance(response, StatusResponse):
|
||||||
|
|
||||||
# Global Head
|
# Global Head
|
||||||
if data['time'] is not None:
|
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
|
# AC Data
|
||||||
phase_id = 0
|
phase_id = 0
|
||||||
|
@ -233,12 +256,16 @@ class MqttOutputPlugin(OutputPluginFactory):
|
||||||
string_id = 0
|
string_id = 0
|
||||||
string_sum_power = 0
|
string_sum_power = 0
|
||||||
for string in data['strings']:
|
for string in data['strings']:
|
||||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'], self.qos, self.ret)
|
if 'name' in string:
|
||||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'], self.qos, self.ret)
|
string_name = string['name'].replace(" ","_")
|
||||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'], self.qos, self.ret)
|
else:
|
||||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'], self.qos, self.ret)
|
string_name = string_id
|
||||||
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_name}/voltage', string['voltage'], self.qos, self.ret)
|
||||||
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'], 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_id = string_id + 1
|
||||||
string_sum_power += string['power']
|
string_sum_power += string['power']
|
||||||
|
|
||||||
|
@ -277,9 +304,10 @@ class VzInverterOutput:
|
||||||
self.channels = dict()
|
self.channels = dict()
|
||||||
|
|
||||||
for channel in config.get('channels', []):
|
for channel in config.get('channels', []):
|
||||||
uid = channel.get('uid')
|
uid = channel.get('uid', None)
|
||||||
ctype = channel.get('type')
|
ctype = channel.get('type')
|
||||||
if uid and ctype:
|
# if uid and ctype:
|
||||||
|
if ctype:
|
||||||
self.channels[ctype] = uid
|
self.channels[ctype] = uid
|
||||||
|
|
||||||
def store_status(self, data, session):
|
def store_status(self, data, session):
|
||||||
|
@ -295,6 +323,9 @@ class VzInverterOutput:
|
||||||
|
|
||||||
ts = int(round(data['time'].timestamp() * 1000))
|
ts = int(round(data['time'].timestamp() * 1000))
|
||||||
|
|
||||||
|
if HOYMILES_DEBUG_LOGGING:
|
||||||
|
logging.info(f'Volkszaehler-Timestamp: {ts}')
|
||||||
|
|
||||||
# AC Data
|
# AC Data
|
||||||
phase_id = 0
|
phase_id = 0
|
||||||
for phase in data['phases']:
|
for phase in data['phases']:
|
||||||
|
@ -327,13 +358,24 @@ class VzInverterOutput:
|
||||||
if data['yield_today'] is not None:
|
if data['yield_today'] is not None:
|
||||||
self.try_publish(ts, f'yield_today', data['yield_today'])
|
self.try_publish(ts, f'yield_today', data['yield_today'])
|
||||||
self.try_publish(ts, f'efficiency', data['efficiency'])
|
self.try_publish(ts, f'efficiency', data['efficiency'])
|
||||||
|
return
|
||||||
|
|
||||||
def try_publish(self, ts, ctype, value):
|
def try_publish(self, ts, ctype, value):
|
||||||
if not ctype in self.channels:
|
if not ctype in self.channels:
|
||||||
|
if HOYMILES_DEBUG_LOGGING:
|
||||||
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
|
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
|
||||||
return
|
return
|
||||||
|
|
||||||
uid = self.channels[ctype]
|
uid = self.channels[ctype]
|
||||||
url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}'
|
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:
|
try:
|
||||||
r = self.session.get(url)
|
r = self.session.get(url)
|
||||||
if r.status_code == 404:
|
if r.status_code == 404:
|
||||||
|
@ -344,6 +386,7 @@ class VzInverterOutput:
|
||||||
raise ValueError(f'Transmit result {url}')
|
raise ValueError(f'Transmit result {url}')
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}')
|
raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}')
|
||||||
|
return
|
||||||
|
|
||||||
class VolkszaehlerOutputPlugin(OutputPluginFactory):
|
class VolkszaehlerOutputPlugin(OutputPluginFactory):
|
||||||
def __init__(self, config, **params):
|
def __init__(self, config, **params):
|
||||||
|
@ -364,13 +407,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.inverters = dict()
|
|
||||||
|
|
||||||
|
self.inverters = dict()
|
||||||
for inverterconfig in config.get('inverters', []):
|
for inverterconfig in config.get('inverters', []):
|
||||||
serial = inverterconfig.get('serial')
|
serial = inverterconfig.get('serial')
|
||||||
output = VzInverterOutput(inverterconfig, self.session)
|
output = VzInverterOutput(inverterconfig, self.session)
|
||||||
self.inverters[serial] = output
|
self.inverters[serial] = output
|
||||||
|
|
||||||
|
def disco(self, **params):
|
||||||
|
self.session.close() # closing the connection
|
||||||
|
return
|
||||||
|
|
||||||
def store_status(self, response, **params):
|
def store_status(self, response, **params):
|
||||||
"""
|
"""
|
||||||
Publish StatusResponse object
|
Publish StatusResponse object
|
||||||
|
@ -395,3 +442,4 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
|
||||||
output.store_status(data, self.session)
|
output.store_status(data, self.session)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logging.warning('Could not send data to volkszaehler instance: %s' % e)
|
logging.warning('Could not send data to volkszaehler instance: %s' % e)
|
||||||
|
return
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue