1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
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
|