Browse Source

Merge branch 'modem-man-gmx-main' into development02

pull/635/head
lumapu 2 years ago
parent
commit
9f8371fdef
  1. 35
      Getting_Started.md
  2. BIN
      doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png
  3. BIN
      doc/ESP8266_nRF24L01+_Schaltplan.jpg
  4. 37
      tools/rpi/ahoy.service
  5. 16
      tools/rpi/ahoy.yml.example
  6. 8
      tools/rpi/hoymiles/__init__.py
  7. 54
      tools/rpi/hoymiles/__main__.py
  8. 79
      tools/rpi/hoymiles/decoders/__init__.py
  9. 56
      tools/rpi/hoymiles/outputs.py

35
Getting_Started.md

@ -6,9 +6,12 @@
- [Things needed](#things-needed) - [Things needed](#things-needed)
- [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there) - [There are fake NRF24L01+ Modules out there](#there-are-fake-nrf24l01-modules-out-there)
- [Wiring things up](#wiring-things-up) - [Wiring things up](#wiring-things-up)
- [ESP8266 wiring example](#esp8266-wiring-example) - [ESP8266 wiring example on WEMOS D1](#esp8266-wiring-example)
- [Schematic](#schematic) - [Schematic](#schematic)
- [Symbolic view](#symbolic-view) - [Symbolic view](#symbolic-view)
- [ESP8266 wiring example on 30pin Lolin NodeMCU v3](#esp8266-wiring-example-2)
- [Schematic](#schematic-2)
- [Symbolic view](#symbolic-view-2)
- [ESP32 wiring example](#esp32-wiring-example) - [ESP32 wiring example](#esp32-wiring-example)
- [Schematic](#schematic-1) - [Schematic](#schematic-1)
- [Symbolic view](#symbolic-view-1) - [Symbolic view](#symbolic-view-1)
@ -69,8 +72,9 @@ Make sure the NRF24L01+ module has the "+" in its name as we depend on the 250kb
| **Parts** | **Price** | | **Parts** | **Price** |
| --- | --- | | --- | --- |
| D1 ESP8266 Mini WLAN Board Mikrokontroller | 4,40 Euro | | D1 ESP8266 Mini WLAN Board Microcontroller | 4,40 Euro |
| NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro | | NRF24L01+ SMD Modul 2,4 GHz Wi-Fi Funkmodul | 3,45 Euro |
| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro | | Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **10,34 Euro** | | **Total costs** | **10,34 Euro** |
@ -80,6 +84,7 @@ To also run our sister project OpenDTU and be upwards compatible for the future
| --- | --- | | --- | --- |
| ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro | | ESP32 Dev Board NodeMCU WROOM32 WiFi | 7,90 Euro |
| NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro | | NRF24L01+ PA LNA SMA mit Antenne Long | 4,50 Euro |
| 100µF / 10V Capacitor Kondensator | 0,15 Euro |
| Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro | | Jumper Wire Steckbrücken Steckbrett weiblich-weiblich | 2,49 Euro |
| **Total costs** | **14,89 Euro** | | **Total costs** | **14,89 Euro** |
@ -89,6 +94,18 @@ Watch out, there are some fake NRF24L01+ Modules out there that seem to use rebr
An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).<br/> An example can be found in [Issue #230](https://github.com/lumapu/ahoy/issues/230).<br/>
You are welcome to add more examples of faked chips. We will add that information here.<br/> You are welcome to add more examples of faked chips. We will add that information here.<br/>
Some users reported better connection or longer range through more walls when using the
"E01-ML01DP5" EBYTE 2,4 GHz Wireless Modul nRF24L01 + PA + LNA RF Modul, SMA-K Antenna connector,
which has an eye-catching HF cover. But beware: It comes without the antenna!
In any case you should stabilize the Vcc power by a capacitor and don't exceed the Amplifier Power Level "LOW".
Users reporting good connection over 10m through walls / ceilings with Amplifier Power Level "MIN".
It is not always the bigger the better...
Power levels "HIGH" and "MAX" are meant to wirings where the nRF24 is supplied by an extra 3.3 Volt regulator.
The bultin regulator on ESP boards has only low reserves in case WiFi and nRF are sending simultaneously.
If you operate additional interfaces like a display, the reserve is again reduced.
## Wiring things up ## Wiring things up
The NRF24L01+ radio module is connected to the standard SPI pins: The NRF24L01+ radio module is connected to the standard SPI pins:
@ -107,7 +124,7 @@ Additional, there are 3 pins, which can be set individual:
*These pins can be changed from the /setup URL.* *These pins can be changed from the /setup URL.*
#### ESP8266 wiring example #### ESP8266 wiring example on WEMOS D1
This is an example wiring using a Wemos D1 mini.<br> This is an example wiring using a Wemos D1 mini.<br>
@ -119,6 +136,18 @@ This is an example wiring using a Wemos D1 mini.<br>
![Symbolic](doc/AhoyWemos_Steckplatine.jpg) ![Symbolic](doc/AhoyWemos_Steckplatine.jpg)
#### ESP8266 wiring example on 30pin Lolin NodeMCU v3
This is an example wiring using a NodeMCU V3.<br>
##### Schematic
![Schematic](doc/ESP8266_nRF24L01+_Schaltplan.jpg)
##### Symbolic view
![Symbolic](doc/ESP8266_nRF24L01+_bb.png)
#### ESP32 wiring example #### ESP32 wiring example
Example wiring for a 38pin ESP32 module Example wiring for a 38pin ESP32 module

BIN
doc/ESP8266_nRF24L01+_LolinNodeMCUv3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

BIN
doc/ESP8266_nRF24L01+_Schaltplan.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

37
tools/rpi/ahoy.service

@ -0,0 +1,37 @@
######################################################################
# systemd.service configuration for ahoy (lumapu)
# users can modify the lines:
# Description
# ExecStart (example: name of config file)
# 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
# $ systemctl --user status ahoy
# $ systemctl --user enable ahoy
# $ systemctl --user start ahoy
# $ systemctl --user status ahoy
#
# 2023.01 <PaeserBastelstube>
######################################################################
[Unit]
Description=ahoy (lumapu) as Service
After=network.target local-fs.target time-sync.target
[Service]
ExecStart=/usr/bin/env python3 -um hoymiles --log-transactions --verbose --config ahoy.yml
RestartSec=10
Restart=on-failure
Type=simple
# WorkingDirectory must be an absolute path - not relative path
WorkingDirectory=/home/pi/ahoy/tools/rpi
EnvironmentFile=/etc/environment
[Install]
WantedBy=default.target

16
tools/rpi/ahoy.yml.example

@ -73,7 +73,17 @@ ahoy:
inverters: inverters:
- name: 'balkon' - name: 'balkon'
serial: 114172220003 serial: 114172220003
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 'hoymiles/{serial}' topic: 'hoymiles/114172221234' # 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_name: 'String 2 right' # String 2 name
s_maxpower: 400 # String 2 max power in Wp
- s_name: 'String 3 up' # String 3 name
s_maxpower: 405 # String 3 max power in Wp
- s_name: 'String 4 down' # String 4 name
s_maxpower: 410 # String 4 max power in Wp

8
tools/rpi/hoymiles/__init__.py

@ -144,6 +144,9 @@ class ResponseDecoder(ResponseDecoderFactory):
def __init__(self, response, **params): def __init__(self, response, **params):
"""Initialize ResponseDecoder""" """Initialize ResponseDecoder"""
ResponseDecoderFactory.__init__(self, response, **params) ResponseDecoderFactory.__init__(self, response, **params)
self.inv_name=params.get('inverter_name', None)
self.dtu_ser=params.get('dtu_ser', None)
self.strings=params.get('strings', None)
def decode(self): def decode(self):
""" """
@ -164,7 +167,10 @@ class ResponseDecoder(ResponseDecoderFactory):
return device(self.response, return device(self.response,
time_rx=self.time_rx, time_rx=self.time_rx,
inverter_ser=self.inverter_ser inverter_ser=self.inverter_ser,
inverter_name=self.inv_name,
dtu_ser=self.dtu_ser,
strings=self.strings
) )
class InverterPacketFragment: class InverterPacketFragment:

54
tools/rpi/hoymiles/__main__.py

@ -118,6 +118,8 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
:type retries: int :type retries: int
""" """
inverter_ser = inverter.get('serial') inverter_ser = inverter.get('serial')
inverter_name = inverter.get('name')
inverter_strings = inverter.get('strings')
# Queue at least status data request # Queue at least status data request
inv_str = str(inverter_ser) inv_str = str(inverter_ser)
@ -161,25 +163,17 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
decoder = hoymiles.ResponseDecoder(response, decoder = hoymiles.ResponseDecoder(response,
request=com.request, request=com.request,
inverter_ser=inverter_ser inverter_ser=inverter_ser,
inverter_name=inverter_name,
dtu_ser=dtu_ser,
strings=inverter_strings
) )
result = decoder.decode() result = decoder.decode()
if isinstance(result, hoymiles.decoders.StatusResponse): if isinstance(result, hoymiles.decoders.StatusResponse):
data = result.__dict__() data = result.__dict__()
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
dbg = f'{c_datetime} Decoded: temp={data["temperature"]}, total={data["energy_total"]/1000:.3f}' logging.debug(f'{c_datetime} Decoded: {result.__dict__()}')
if data['powerfactor'] is not None:
dbg += f', pf={data["powerfactor"]}'
phase_id = 0
for phase in data['phases']:
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']:
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
logging.debug(dbg)
if 'event_count' in data: if 'event_count' in data:
if event_message_index[inv_str] < data['event_count']: if event_message_index[inv_str] < data['event_count']:
@ -187,8 +181,9 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str])) command_queue[inv_str].append(hoymiles.compose_send_time_payload(InfoCommands.AlarmData, alarm_id=event_message_index[inv_str]))
if mqtt_client: if mqtt_client:
mqtt_send_status(mqtt_client, inverter_ser, data, # mqtt_send_status(mqtt_client, inverter_ser, data, topic=inverter.get('mqtt', {}).get('topic', None))
topic=inverter.get('mqtt', {}).get('topic', None)) mqtt_client.store_status(result, topic=inverter.get('mqtt', {}).get('topic', None))
if influx_client: if influx_client:
influx_client.store_status(result) influx_client.store_status(result)
@ -209,21 +204,27 @@ def mqtt_send_status(broker, inverter_ser, data, topic=None):
if not topic: if not topic:
topic = f'hoymiles/{inverter_ser}' topic = f'hoymiles/{inverter_ser}'
# Global Head
if data['time'] is not None:
broker.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"))
# AC Data # AC Data
phase_id = 0 phase_id = 0
for phase in data['phases']: for phase in data['phases']:
broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) broker.publish(f'{topic}/emeter/{phase_id}/power', phase['power'])
broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage']) broker.publish(f'{topic}/emeter/{phase_id}/voltage', phase['voltage'])
broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) broker.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
broker.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'])
phase_id = phase_id + 1 phase_id = phase_id + 1
# DC Data # DC Data
string_id = 0 string_id = 0
for string in data['strings']: for string in data['strings']:
broker.publish(f'{topic}/emeter-dc/{string_id}/total', string['energy_total']/1000)
broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage']) broker.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'])
broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) broker.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
broker.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
broker.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'])
broker.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000)
string_id = string_id + 1 string_id = string_id + 1
# Global # Global
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
@ -328,8 +329,6 @@ if __name__ == '__main__':
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)
mqtt_client = None
event_message_index = {} event_message_index = {}
command_queue = {} command_queue = {}
mqtt_command_topic_subs = [] mqtt_command_topic_subs = []
@ -339,18 +338,11 @@ if __name__ == '__main__':
if global_config.verbose: if global_config.verbose:
hoymiles.HOYMILES_DEBUG_LOGGING=True hoymiles.HOYMILES_DEBUG_LOGGING=True
mqtt_config = ahoy_config.get('mqtt', []) mqtt_client = None
if not mqtt_config.get('disabled', False): mqtt_config = ahoy_config.get('mqtt', {})
mqtt_client = paho.mqtt.client.Client() if mqtt_config and not mqtt_config.get('disabled', False):
from .outputs import MqttOutputPlugin
if mqtt_config.get('useTLS',False): mqtt_client = MqttOutputPlugin(mqtt_config)
mqtt_client.tls_set()
mqtt_client.tls_insecure_set(mqtt_config.get('insecureTLS',False))
mqtt_client.username_pw_set(mqtt_config.get('user', None), mqtt_config.get('password', None))
mqtt_client.connect(mqtt_config.get('host', '127.0.0.1'), mqtt_config.get('port', 1883))
mqtt_client.loop_start()
mqtt_client.on_message = mqtt_on_command
influx_client = None influx_client = None
influx_config = ahoy_config.get('influxdb', {}) influx_config = ahoy_config.get('influxdb', {})

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

@ -74,9 +74,11 @@ class Response:
self.inverter_ser = params.get('inverter_ser', None) self.inverter_ser = params.get('inverter_ser', None)
self.inverter_name = params.get('inverter_name', None) self.inverter_name = params.get('inverter_name', None)
self.dtu_ser = params.get('dtu_ser', None) self.dtu_ser = params.get('dtu_ser', None)
self.response = args[0] self.response = args[0]
strings = params.get('strings', None)
self.inv_strings = strings
if isinstance(params.get('time_rx', None), datetime): if isinstance(params.get('time_rx', None), datetime):
self.time_rx = params['time_rx'] self.time_rx = params['time_rx']
else: else:
@ -91,7 +93,7 @@ class Response:
class StatusResponse(Response): class StatusResponse(Response):
"""Inverter StatusResponse object""" """Inverter StatusResponse object"""
e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor'] e_keys = ['voltage','current','power','energy_total','energy_daily','powerfactor', 'reactive_power', 'irradiation']
temperature = None temperature = None
frequency = None frequency = None
powerfactor = None powerfactor = None
@ -333,18 +335,29 @@ class HardwareInfoResponse(UnknownResponse):
{ FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 }, { FLD_FW_VERSION, UNIT_NONE, CH0, 0, 2, 1 },
{ FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 }, { FLD_FW_BUILD_YEAR, UNIT_NONE, CH0, 2, 2, 1 },
{ FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 }, { FLD_FW_BUILD_MONTH_DAY, UNIT_NONE, CH0, 4, 2, 1 },
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 } { FLD_FW_Build_Hour_Minute, UNIT_NONE, CH0, 6, 2, 1 },
{ FLD_HW_ID, UNIT_NONE, CH0, 8, 2, 1 },
{ FLD_unknown, UNIT_NONE, CH0, 10, 2, 1 },
{ FLD_unknown, UNIT_NONE, CH0, 12, 2, 1 },
{ FLD_CRC-M, UNIT_NONE, CH0, 14, 2, 1 }
}; };
self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1') self.response = bytes('\x27\x1a\x07\xe5\x04\x4d\x03\x4a\x00\x68\x00\x00\x00\x00\xe6\xfb', 'latin1')
""" """
fw_version, fw_build_yyyy, fw_build_mmdd, unknown, 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])
responce_info = self.response
logging.debug(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}')
fw_version_maj = int((fw_version / 10000)) fw_version_maj = int((fw_version / 10000))
fw_version_min = int((fw_version % 10000) / 100) fw_version_min = int((fw_version % 10000) / 100)
fw_version_pat = int((fw_version % 100)) fw_version_pat = int((fw_version % 100))
fw_build_mm = int(fw_build_mmdd / 100) fw_build_mm = int(fw_build_mmdd / 100)
fw_build_dd = int(fw_build_mmdd % 100) fw_build_dd = int(fw_build_mmdd % 100)
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}') fw_build_HH = int(fw_build_hhmm / 100)
fw_build_MM = int(fw_build_hhmm % 100)
logging.debug(f'Firmware: {fw_version_maj}.{fw_version_min}.{fw_version_pat} '\
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}')
class DebugDecodeAny(UnknownResponse): class DebugDecodeAny(UnknownResponse):
"""Default decoder""" """Default decoder"""
@ -420,6 +433,12 @@ class Hm300Decode0B(StatusResponse):
def dc_energy_daily_0(self): def dc_energy_daily_0(self):
""" String 1 daily energy in Wh """ """ String 1 daily energy in Wh """
return self.unpack('>H', 12)[0] return self.unpack('>H', 12)[0]
@property
def dc_irradiation_0(self):
""" String 1 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property @property
def ac_voltage_0(self): def ac_voltage_0(self):
@ -438,6 +457,10 @@ class Hm300Decode0B(StatusResponse):
""" Grid frequency in Hertz """ """ Grid frequency in Hertz """
return self.unpack('>H', 16)[0]/100 return self.unpack('>H', 16)[0]/100
@property @property
def ac_reactive_power_0(self):
""" reactive power """
return self.unpack('>H', 20)[0]/10
@property
def temperature(self): def temperature(self):
""" Inverter temperature in °C """ """ Inverter temperature in °C """
return self.unpack('>h', 26)[0]/10 return self.unpack('>h', 26)[0]/10
@ -482,6 +505,12 @@ class Hm600Decode0B(StatusResponse):
def dc_energy_daily_0(self): def dc_energy_daily_0(self):
""" String 1 daily energy in Wh """ """ String 1 daily energy in Wh """
return self.unpack('>H', 22)[0] return self.unpack('>H', 22)[0]
@property
def dc_irradiation_0(self):
""" String 1 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property @property
def dc_voltage_1(self): def dc_voltage_1(self):
@ -503,6 +532,12 @@ class Hm600Decode0B(StatusResponse):
def dc_energy_daily_1(self): def dc_energy_daily_1(self):
""" String 2 daily energy in Wh """ """ String 2 daily energy in Wh """
return self.unpack('>H', 24)[0] return self.unpack('>H', 24)[0]
@property
def dc_irradiation_1(self):
""" String 2 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
@property @property
def ac_voltage_0(self): def ac_voltage_0(self):
@ -511,7 +546,7 @@ class Hm600Decode0B(StatusResponse):
@property @property
def ac_current_0(self): def ac_current_0(self):
""" Phase 1 ampere """ """ Phase 1 ampere """
return self.unpack('>H', 34)[0]/10 return self.unpack('>H', 34)[0]/100
@property @property
def ac_power_0(self): def ac_power_0(self):
""" Phase 1 watts """ """ Phase 1 watts """
@ -521,6 +556,10 @@ class Hm600Decode0B(StatusResponse):
""" Grid frequency in Hertz """ """ Grid frequency in Hertz """
return self.unpack('>H', 28)[0]/100 return self.unpack('>H', 28)[0]/100
@property @property
def ac_reactive_power_0(self):
""" reactive power """
return self.unpack('>H', 32)[0]/10
@property
def powerfactor(self): def powerfactor(self):
""" Powerfactor """ """ Powerfactor """
return self.unpack('>H', 36)[0]/1000 return self.unpack('>H', 36)[0]/1000
@ -573,6 +612,12 @@ class Hm1200Decode0B(StatusResponse):
def dc_energy_daily_0(self): def dc_energy_daily_0(self):
""" String 1 daily energy in Wh """ """ String 1 daily energy in Wh """
return self.unpack('>H', 20)[0] return self.unpack('>H', 20)[0]
@property
def dc_irradiation_0(self):
""" String 1 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property @property
def dc_voltage_1(self): def dc_voltage_1(self):
@ -594,6 +639,12 @@ class Hm1200Decode0B(StatusResponse):
def dc_energy_daily_1(self): def dc_energy_daily_1(self):
""" String 2 daily energy in Wh """ """ String 2 daily energy in Wh """
return self.unpack('>H', 22)[0] return self.unpack('>H', 22)[0]
@property
def dc_irradiation_0(self):
""" String 2 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
@property @property
def dc_voltage_2(self): def dc_voltage_2(self):
@ -615,6 +666,12 @@ class Hm1200Decode0B(StatusResponse):
def dc_energy_daily_2(self): def dc_energy_daily_2(self):
""" String 3 daily energy in Wh """ """ String 3 daily energy in Wh """
return self.unpack('>H', 42)[0] return self.unpack('>H', 42)[0]
@property
def dc_irradiation_0(self):
""" String 3 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3)
@property @property
def dc_voltage_3(self): def dc_voltage_3(self):
@ -636,6 +693,12 @@ class Hm1200Decode0B(StatusResponse):
def dc_energy_daily_3(self): def dc_energy_daily_3(self):
""" String 4 daily energy in Wh """ """ String 4 daily energy in Wh """
return self.unpack('>H', 44)[0] return self.unpack('>H', 44)[0]
@property
def dc_irradiation_0(self):
""" String 4 irratiation in percent """
if self.inv_strings is None:
return None
return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3)
@property @property
def ac_voltage_0(self): def ac_voltage_0(self):
@ -654,6 +717,10 @@ class Hm1200Decode0B(StatusResponse):
""" Grid frequency in Hertz """ """ Grid frequency in Hertz """
return self.unpack('>H', 48)[0]/100 return self.unpack('>H', 48)[0]/100
@property @property
def ac_reactive_power_0(self):
""" reactive power """
return self.unpack('>H', 52)[0]/10
@property
def powerfactor(self): def powerfactor(self):
""" Powerfactor """ """ Powerfactor """
return self.unpack('>H', 56)[0]/1000 return self.unpack('>H', 56)[0]/1000

56
tools/rpi/hoymiles/outputs.py

@ -101,26 +101,32 @@ class InfluxOutputPlugin(OutputPluginFactory):
# AC Data # AC Data
phase_id = 0 phase_id = 0
for phase in data['phases']: 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=voltage value={phase["voltage"]} {ctime}')
data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}') data_stack.append(f'{measurement},phase={phase_id},type=current value={phase["current"]} {ctime}')
data_stack.append(f'{measurement},phase={phase_id},type=power value={phase["power"]} {ctime}')
data_stack.append(f'{measurement},phase={phase_id},type=Q_AC value={phase["reactive_power"]} {ctime}')
phase_id = phase_id + 1 phase_id = phase_id + 1
# DC Data # DC Data
string_id = 0 string_id = 0
for string in data['strings']: 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=voltage value={string["voltage"]:.3f} {ctime}')
data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {ctime}') data_stack.append(f'{measurement},string={string_id},type=current value={string["current"]:3f} {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=YieldDay value={string["energy_daily"]:.2f} {ctime}')
data_stack.append(f'{measurement},string={string_id},type=YieldTotal value={string["energy_total"]/1000:.4f} {ctime}')
if 'irradiation' in string:
data_stack.append(f'{measurement},string={string_id},type=Irradiation value={string["irradiation"]:.2f} {ctime}')
string_id = string_id + 1 string_id = string_id + 1
# Global # Global
if data['event_count'] is not None: if data['event_count'] is not None:
data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}') data_stack.append(f'{measurement},type=total_events value={data["event_count"]} {ctime}')
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}') data_stack.append(f'{measurement},type=pf value={data["powerfactor"]:f} {ctime}')
data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}') data_stack.append(f'{measurement},type=frequency value={data["frequency"]:.3f} {ctime}')
data_stack.append(f'{measurement},type=temperature value={data["temperature"]:.2f} {ctime}')
data_stack.append(f'{measurement},type=Temp value={data["temperature"]:.2f} {ctime}')
if data['energy_total'] is not None: if data['energy_total'] is not None:
data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}') data_stack.append(f'{measurement},type=total value={data["energy_total"]/1000:.3f} {ctime}')
@ -135,7 +141,7 @@ class MqttOutputPlugin(OutputPluginFactory):
""" Mqtt output plugin """ """ Mqtt output plugin """
client = None client = None
def __init__(self, *args, **params): def __init__(self, config, **params):
""" """
Initialize MqttOutputPlugin Initialize MqttOutputPlugin
@ -156,11 +162,14 @@ class MqttOutputPlugin(OutputPluginFactory):
:param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser}) :param topic: custom mqtt topic prefix (default: hoymiles/{inverter_ser})
:type topic: str :type topic: str
""" """
super().__init__(*args, **params) super().__init__(**params)
mqtt_client = paho.mqtt.client.Client() mqtt_client = paho.mqtt.client.Client()
mqtt_client.username_pw_set(params.get('user', None), params.get('password', None)) if config.get('useTLS',False):
mqtt_client.connect(params.get('host', '127.0.0.1'), params.get('port', 1883)) mqtt_client.tls_set()
mqtt_client.tls_insecure_set(config.get('insecureTLS',False))
mqtt_client.username_pw_set(config.get('user', None), config.get('password', None))
mqtt_client.connect(config.get('host', '127.0.0.1'), config.get('port', 1883))
mqtt_client.loop_start() mqtt_client.loop_start()
self.client = mqtt_client self.client = mqtt_client
@ -180,8 +189,11 @@ class MqttOutputPlugin(OutputPluginFactory):
raise ValueError('Data needs to be instance of StatusResponse') raise ValueError('Data needs to be instance of StatusResponse')
data = response.__dict__() data = response.__dict__()
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
topic = params.get('topic', f'hoymiles/{data["inverter_ser"]}') # Global Head
if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"))
# AC Data # AC Data
phase_id = 0 phase_id = 0
@ -189,21 +201,27 @@ class MqttOutputPlugin(OutputPluginFactory):
self.client.publish(f'{topic}/emeter/{phase_id}/power', phase['power']) 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}/voltage', phase['voltage'])
self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current']) self.client.publish(f'{topic}/emeter/{phase_id}/current', phase['current'])
self.client.publish(f'{topic}/emeter/{phase_id}/Q_AC', phase['reactive_power'])
phase_id = phase_id + 1 phase_id = phase_id + 1
# DC Data # DC Data
string_id = 0 string_id = 0
for string in data['strings']: 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}/voltage', string['voltage'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current']) self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'])
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000)
if 'irradiation' in string:
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'])
string_id = string_id + 1 string_id = string_id + 1
# Global # Global
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
self.client.publish(f'{topic}/pf', data['powerfactor']) self.client.publish(f'{topic}/pf', data['powerfactor'])
self.client.publish(f'{topic}/frequency', data['frequency']) self.client.publish(f'{topic}/frequency', data['frequency'])
self.client.publish(f'{topic}/temperature', data['temperature'])
self.client.publish(f'{topic}/Temp', data['temperature'])
if data['energy_total'] is not None: if data['energy_total'] is not None:
self.client.publish(f'{topic}/total', data['energy_total']/1000) self.client.publish(f'{topic}/total', data['energy_total']/1000)
@ -241,26 +259,30 @@ class VzInverterOutput:
# AC Data # AC Data
phase_id = 0 phase_id = 0
for phase in data['phases']: for phase in data['phases']:
self.try_publish(ts, f'ac_power{phase_id}', phase['power'])
self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage']) self.try_publish(ts, f'ac_voltage{phase_id}', phase['voltage'])
self.try_publish(ts, f'ac_current{phase_id}', phase['current']) self.try_publish(ts, f'ac_current{phase_id}', phase['current'])
self.try_publish(ts, f'ac_power{phase_id}', phase['power'])
self.try_publish(ts, f'ac_Q{phase_id}', phase['reactive_power'])
phase_id = phase_id + 1 phase_id = phase_id + 1
# DC Data # DC Data
string_id = 0 string_id = 0
for string in data['strings']: for string in data['strings']:
self.try_publish(ts, f'dc_power{string_id}', string['power'])
self.try_publish(ts, f'dc_voltage{string_id}', string['voltage']) self.try_publish(ts, f'dc_voltage{string_id}', string['voltage'])
self.try_publish(ts, f'dc_current{string_id}', string['current']) self.try_publish(ts, f'dc_current{string_id}', string['current'])
self.try_publish(ts, f'dc_total{string_id}', string['energy_total']) self.try_publish(ts, f'dc_power{string_id}', string['power'])
self.try_publish(ts, f'dc_daily{string_id}', string['energy_daily']) self.try_publish(ts, f'dc_YieldDay{string_id}', string['energy_daily'])
self.try_publish(ts, f'dc_YieldTotal{string_id}', string['energy_total'])
if 'irradiation' in string:
self.try_publish(ts, f'dc_Irradiation{string_id}', string['irradiation'])
string_id = string_id + 1 string_id = string_id + 1
# Global # Global
if data['powerfactor'] is not None: if data['powerfactor'] is not None:
self.try_publish(ts, f'powerfactor', data['powerfactor']) self.try_publish(ts, f'powerfactor', data['powerfactor'])
self.try_publish(ts, f'frequency', data['frequency']) self.try_publish(ts, f'frequency', data['frequency'])
self.try_publish(ts, f'temperature', data['temperature'])
self.try_publish(ts, f'Temp', data['temperature'])
if data['energy_total'] is not None: if data['energy_total'] is not None:
self.try_publish(ts, f'total', data['energy_total']) self.try_publish(ts, f'total', data['energy_total'])

Loading…
Cancel
Save