196 lines
7.1 KiB
Python
196 lines
7.1 KiB
Python
import time
|
|
import json
|
|
import datetime
|
|
try:
|
|
import inverterd
|
|
except:
|
|
pass
|
|
|
|
from typing import Optional
|
|
from .._module import MqttModule
|
|
from .._node import MqttNode
|
|
from .._payload import MqttPayload, bit_field
|
|
try:
|
|
from homekit.database import InverterDatabase
|
|
except:
|
|
pass
|
|
|
|
_mult_10 = lambda n: int(n*10)
|
|
_div_10 = lambda n: n/10
|
|
|
|
|
|
MODULE_NAME = 'MqttInverterModule'
|
|
|
|
STATUS_TOPIC = 'status'
|
|
GENERATION_TOPIC = 'generation'
|
|
|
|
|
|
class MqttInverterStatusPayload(MqttPayload):
|
|
# 46 bytes
|
|
FORMAT = 'IHHHHHHBHHHHHBHHHHHHHH'
|
|
|
|
PACKER = {
|
|
'grid_voltage': _mult_10,
|
|
'grid_freq': _mult_10,
|
|
'ac_output_voltage': _mult_10,
|
|
'ac_output_freq': _mult_10,
|
|
'battery_voltage': _mult_10,
|
|
'battery_voltage_scc': _mult_10,
|
|
'battery_voltage_scc2': _mult_10,
|
|
'pv1_input_voltage': _mult_10,
|
|
'pv2_input_voltage': _mult_10
|
|
}
|
|
UNPACKER = {
|
|
'grid_voltage': _div_10,
|
|
'grid_freq': _div_10,
|
|
'ac_output_voltage': _div_10,
|
|
'ac_output_freq': _div_10,
|
|
'battery_voltage': _div_10,
|
|
'battery_voltage_scc': _div_10,
|
|
'battery_voltage_scc2': _div_10,
|
|
'pv1_input_voltage': _div_10,
|
|
'pv2_input_voltage': _div_10
|
|
}
|
|
|
|
time: int
|
|
grid_voltage: float
|
|
grid_freq: float
|
|
ac_output_voltage: float
|
|
ac_output_freq: float
|
|
ac_output_apparent_power: int
|
|
ac_output_active_power: int
|
|
output_load_percent: int
|
|
battery_voltage: float
|
|
battery_voltage_scc: float
|
|
battery_voltage_scc2: float
|
|
battery_discharge_current: int
|
|
battery_charge_current: int
|
|
battery_capacity: int
|
|
inverter_heat_sink_temp: int
|
|
mppt1_charger_temp: int
|
|
mppt2_charger_temp: int
|
|
pv1_input_power: int
|
|
pv2_input_power: int
|
|
pv1_input_voltage: float
|
|
pv2_input_voltage: float
|
|
|
|
# H
|
|
mppt1_charger_status: bit_field(0, 16, 2)
|
|
mppt2_charger_status: bit_field(0, 16, 2)
|
|
battery_power_direction: bit_field(0, 16, 2)
|
|
dc_ac_power_direction: bit_field(0, 16, 2)
|
|
line_power_direction: bit_field(0, 16, 2)
|
|
load_connected: bit_field(0, 16, 1)
|
|
|
|
|
|
class MqttInverterGenerationPayload(MqttPayload):
|
|
# 8 bytes
|
|
FORMAT = 'II'
|
|
|
|
time: int
|
|
wh: int
|
|
|
|
|
|
class MqttInverterModule(MqttModule):
|
|
_status_poll_freq: int
|
|
_generation_poll_freq: int
|
|
_inverter: Optional[inverterd.Client]
|
|
_database: Optional[InverterDatabase]
|
|
_gen_prev: float
|
|
|
|
def __init__(self, status_poll_freq=0, generation_poll_freq=0):
|
|
super().__init__(tick_interval=status_poll_freq)
|
|
self._status_poll_freq = status_poll_freq
|
|
self._generation_poll_freq = generation_poll_freq
|
|
|
|
# this defines whether this is a publisher or a subscriber
|
|
if status_poll_freq > 0:
|
|
self._inverter = inverterd.Client()
|
|
self._inverter.connect()
|
|
self._inverter.format(inverterd.Format.SIMPLE_JSON)
|
|
self._database = None
|
|
else:
|
|
self._inverter = None
|
|
self._database = InverterDatabase()
|
|
|
|
self._gen_prev = 0
|
|
|
|
def on_connect(self, mqtt: MqttNode):
|
|
super().on_connect(mqtt)
|
|
if not self._inverter:
|
|
mqtt.subscribe_module(STATUS_TOPIC, self)
|
|
mqtt.subscribe_module(GENERATION_TOPIC, self)
|
|
|
|
def tick(self):
|
|
if not self._inverter:
|
|
return
|
|
|
|
# read status
|
|
now = time.time()
|
|
try:
|
|
raw = self._inverter.exec('get-status')
|
|
except inverterd.InverterError as e:
|
|
self._logger.error(f'inverter error: {str(e)}')
|
|
# TODO send to server
|
|
return
|
|
|
|
data = json.loads(raw)['data']
|
|
status = MqttInverterStatusPayload(time=round(now), **data)
|
|
self._mqtt_node_ref.publish(STATUS_TOPIC, status.pack())
|
|
|
|
# read today's generation stat
|
|
now = time.time()
|
|
if self._gen_prev == 0 or now - self._gen_prev >= self._generation_poll_freq:
|
|
self._gen_prev = now
|
|
today = datetime.date.today()
|
|
try:
|
|
raw = self._inverter.exec('get-day-generated', (today.year, today.month, today.day))
|
|
except inverterd.InverterError as e:
|
|
self._logger.error(f'inverter error: {str(e)}')
|
|
# TODO send to server
|
|
return
|
|
|
|
data = json.loads(raw)['data']
|
|
gen = MqttInverterGenerationPayload(time=round(now), wh=data['wh'])
|
|
self._mqtt_node_ref.publish(GENERATION_TOPIC, gen.pack())
|
|
|
|
def handle_payload(self, mqtt: MqttNode, topic: str, payload: bytes) -> Optional[MqttPayload]:
|
|
home_id = 1 # legacy compat
|
|
|
|
if topic == STATUS_TOPIC:
|
|
s = MqttInverterStatusPayload.unpack(payload)
|
|
self._database.add_status(home_id=home_id,
|
|
client_time=s.time,
|
|
grid_voltage=int(s.grid_voltage*10),
|
|
grid_freq=int(s.grid_freq * 10),
|
|
ac_output_voltage=int(s.ac_output_voltage * 10),
|
|
ac_output_freq=int(s.ac_output_freq * 10),
|
|
ac_output_apparent_power=s.ac_output_apparent_power,
|
|
ac_output_active_power=s.ac_output_active_power,
|
|
output_load_percent=s.output_load_percent,
|
|
battery_voltage=int(s.battery_voltage * 10),
|
|
battery_voltage_scc=int(s.battery_voltage_scc * 10),
|
|
battery_voltage_scc2=int(s.battery_voltage_scc2 * 10),
|
|
battery_discharge_current=s.battery_discharge_current,
|
|
battery_charge_current=s.battery_charge_current,
|
|
battery_capacity=s.battery_capacity,
|
|
inverter_heat_sink_temp=s.inverter_heat_sink_temp,
|
|
mppt1_charger_temp=s.mppt1_charger_temp,
|
|
mppt2_charger_temp=s.mppt2_charger_temp,
|
|
pv1_input_power=s.pv1_input_power,
|
|
pv2_input_power=s.pv2_input_power,
|
|
pv1_input_voltage=int(s.pv1_input_voltage * 10),
|
|
pv2_input_voltage=int(s.pv2_input_voltage * 10),
|
|
mppt1_charger_status=s.mppt1_charger_status,
|
|
mppt2_charger_status=s.mppt2_charger_status,
|
|
battery_power_direction=s.battery_power_direction,
|
|
dc_ac_power_direction=s.dc_ac_power_direction,
|
|
line_power_direction=s.line_power_direction,
|
|
load_connected=s.load_connected)
|
|
return s
|
|
|
|
elif topic == GENERATION_TOPIC:
|
|
gen = MqttInverterGenerationPayload.unpack(payload)
|
|
self._database.add_generation(home_id, gen.time, gen.wh)
|
|
return gen
|