|
120 | 120 | DISPATCH_SOC_HIGH_REG = 44110 |
121 | 121 | DISPATCH_SCHEDULE_BASE = 44116 # 6 periods x 14 registers |
122 | 122 | DISPATCH_SCHEDULE_STRIDE = 14 |
| 123 | +DISPATCH_LIMIT_UNIT = 100 |
| 124 | +DISPATCH_LIMIT_DEFAULT = 0xFFFF |
123 | 125 |
|
124 | 126 | # mode -> (44105 value, sign applied to power_watts); modes 3/4: +export/-import |
125 | 127 | DISPATCH_MODES = { |
|
143 | 145 | vol.Optional("disable_discharge"): vol.Coerce(bool), |
144 | 146 | vol.Optional("soc_min"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), |
145 | 147 | vol.Optional("soc_max"): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), |
| 148 | + vol.Optional("import_limit_watts"): vol.All(vol.Coerce(int), vol.Range(min=0, max=240000)), |
| 149 | + vol.Optional("export_limit_watts"): vol.All(vol.Coerce(int), vol.Range(min=0, max=240000)), |
146 | 150 | vol.Optional("failsafe_minutes", default=30): vol.All(vol.Coerce(int), vol.Range(min=1, max=1440)), |
147 | 151 | vol.Optional("host"): vol.Coerce(str), |
148 | 152 | vol.Optional("slave", default=1): vol.Coerce(int), |
@@ -190,6 +194,24 @@ def _s32_words(value: int) -> list[int]: |
190 | 194 | return [(raw >> 16) & 0xFFFF, raw & 0xFFFF] |
191 | 195 |
|
192 | 196 |
|
| 197 | +def _dispatch_system_limits(import_limit_watts, export_limit_watts) -> tuple[int, int, int]: |
| 198 | + """Build 44102/44103/44104. Omitted = switch off + 0xFFFF (inverter default).""" |
| 199 | + |
| 200 | + def raw(watts) -> int: |
| 201 | + return (int(watts) + DISPATCH_LIMIT_UNIT // 2) // DISPATCH_LIMIT_UNIT |
| 202 | + |
| 203 | + switches = 0 |
| 204 | + import_raw = DISPATCH_LIMIT_DEFAULT |
| 205 | + export_raw = DISPATCH_LIMIT_DEFAULT |
| 206 | + if import_limit_watts is not None: |
| 207 | + switches |= 0b01 |
| 208 | + import_raw = raw(import_limit_watts) |
| 209 | + if export_limit_watts is not None: |
| 210 | + switches |= 0b10 |
| 211 | + export_raw = raw(export_limit_watts) |
| 212 | + return switches, import_raw, export_raw |
| 213 | + |
| 214 | + |
193 | 215 | async def async_remove_config_entry_device(hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry) -> bool: |
194 | 216 | """Remove a config entry from a device.""" |
195 | 217 | return True |
@@ -354,15 +376,16 @@ async def service_dispatch(call: ServiceCall) -> None: |
354 | 376 | function_value = _dispatch_function_value(call.data.get("pv_shutdown"), call.data.get("allow_grid_charge"), call.data.get("disable_discharge")) |
355 | 377 | soc_low = int(call.data["soc_min"]) if call.data.get("soc_min") is not None else 0 |
356 | 378 | soc_high = int(call.data["soc_max"]) if call.data.get("soc_max") is not None else 100 |
| 379 | + switches, import_raw, export_raw = _dispatch_system_limits(call.data.get("import_limit_watts"), call.data.get("export_limit_watts")) |
357 | 380 |
|
358 | 381 | # The dispatch block must be written as contiguous chunks (Ver3.4 doc): |
359 | 382 | # scattered single-register writes get silently dropped/re-initialized, |
360 | 383 | # especially under write-queue contention. Two atomic FC16 blocks: |
361 | | - # global 44100-44104 = master, failsafe, no system caps (0xFFFF = default) |
| 384 | + # global 44100-44104 = master, failsafe, limit switch, import/export caps |
362 | 385 | # realtime 44105-44112 = mode, power(S32), function, SOC window |
363 | 386 | # Global first so dispatch is active before the realtime block lands |
364 | 387 | # (the function field is re-initialized unless the master is already on). |
365 | | - await controller.async_write_holding_registers(DISPATCH_MASTER_REG, [1, int(call.data.get("failsafe_minutes", 30)), 0, 0xFFFF, 0xFFFF]) |
| 388 | + await controller.async_write_holding_registers(DISPATCH_MASTER_REG, [1, int(call.data.get("failsafe_minutes", 30)), switches, import_raw, export_raw]) |
366 | 389 | await controller.async_write_holding_registers(DISPATCH_MODE_REG, [mode_value, *_s32_words(power_raw), function_value, soc_low, soc_high, 0, 0]) |
367 | 390 |
|
368 | 391 | async def service_dispatch_stop(call: ServiceCall) -> None: |
|
0 commit comments