summaryrefslogtreecommitdiff
path: root/include/py/homekit/mqtt/module/inverter.py
blob: 29bde0a8730ff3389ae3e291e77ddf818c0006a0 (plain)
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