Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .cspell/custom-dictionary-workspace.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ compareform
configform
connack
Consolas
contribs
coro
cprofile
creds
Expand Down Expand Up @@ -96,6 +97,7 @@ diverter
dlimit
dnoregion
docstrings
docversion
dstamp
dstart
dwindow
Expand Down Expand Up @@ -313,6 +315,7 @@ recp
Redownload
regionid
regname
rels
remainings
remotecontrol
resetmidnight
Expand Down Expand Up @@ -408,6 +411,7 @@ twinx
tzfile
tzpath
unconfigured
unioned
unsmoothed
unstaged
useid
Expand Down
2 changes: 2 additions & 0 deletions apps/predbat/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@
HAS_GATEWAY = False
GatewayMQTT = None
from load_ml_component import LoadMLComponent
from lattice_component import LatticeComponent
from datetime import datetime, timezone, timedelta
import asyncio
import os


COMPONENT_LIST = {
"storage": {"class": StorageComponent, "name": "Storage", "args": {}, "can_restart": True, "phase": 0},
"lattice": {"class": LatticeComponent, "name": "Lattice Device Map", "args": {}, "can_restart": True, "phase": 2},
"db": {
"class": DatabaseManager,
"name": "Database Manager",
Expand Down
6 changes: 6 additions & 0 deletions apps/predbat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
"type": "switch",
"default": False,
},
{
"name": "lattice_projection_enable",
"friendly_name": "Lattice Device Map (experimental)",
"type": "switch",
"default": False,
},
{
"name": "active",
"friendly_name": "Predbat Active",
Expand Down
8 changes: 8 additions & 0 deletions apps/predbat/fox.py
Original file line number Diff line number Diff line change
Expand Up @@ -1618,6 +1618,14 @@ async def publish_data(self):

self.dashboard_item(entity_id, state=state, attributes=attributes, app="fox")

def lattice_fragment(self):
"""Read-only Lattice fragment: FoxESS cloud devices (topology + sensor inventory)."""
from lattice import device_fragment

serials = [d.get("deviceSN") for d in (getattr(self, "device_list", None) or []) if isinstance(d, dict) and d.get("deviceSN")]
devices = [{"serial": str(s), "device_type": "fox", "sensors": [{"capability": "soc", "unit": "%"}, {"capability": "battery_power", "unit": "W"}]} for s in serials]
return device_fragment(devices, provider="fox-cloud", name="FoxESS Cloud", transport="https", preference=1, locality="cloud")

async def write_setting_from_event(self, entity_id, value, is_number=False):
"""
Handle write events
Expand Down
27 changes: 27 additions & 0 deletions apps/predbat/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,33 @@ def _inject_inverter_entities(self, inv, suffix):
self.dashboard_item(f"sensor.{pfx}_battery_charge_today", round(energy.battery_charge_today_wh / 1000.0, 2), attributes=GATEWAY_ATTRIBUTE_TABLE.get("battery_charge_today", {}), app="gateway")
self.dashboard_item(f"sensor.{pfx}_battery_discharge_today", round(energy.battery_discharge_today_wh / 1000.0, 2), attributes=GATEWAY_ATTRIBUTE_TABLE.get("battery_discharge_today", {}), app="gateway")

def lattice_fragment(self):
"""Read-only Lattice fragment: the inverters this gateway sees, plus their sensors.

Maps each battery inverter (identity + type) on a local-Modbus access path and references
the gateway sensor entities that already exist for it. Read-only — no control.
"""
from lattice import device_fragment

sensor_suffixes = [("soc", "%", "soc"), ("battery_power", "W", "battery_power"), ("pv_power", "W", "pv_power"), ("grid_power", "W", "grid_power"), ("load_power", "W", "load_power"), ("temperature", "C", "battery_temperature")]
devices = []
status = getattr(self, "_last_status", None)
for inv in status.inverters if status else []:
if not (inv.battery.ByteSize() > 0 or inv.battery.capacity_wh > 0):
continue
base = "{}_gateway_{}".format(self.prefix, inv.serial[-6:].lower())
sensors = [{"capability": cap, "unit": unit, "entity": "sensor.{}_{}".format(base, suffix)} for cap, unit, suffix in sensor_suffixes]
devices.append({"serial": inv.serial, "device_type": self._lattice_device_type(inv.type), "sensors": sensors})
return device_fragment(devices, provider="local-gateway", name="Local gateway", transport="modbus", preference=10, locality="local")

@staticmethod
def _lattice_device_type(type_value):
"""Best-effort readable device type from the gateway proto inverter-type enum (e.g. 'givenergy_aio')."""
try:
return pb.InverterType.Name(type_value).replace("INVERTER_TYPE_", "").lower()
except Exception:
return "inverter"

def _needs_reconfigure(self, status):
"""Whether automatic_config should (re-)run for this status.

Expand Down
7 changes: 7 additions & 0 deletions apps/predbat/gecloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ def initialize(self, ge_cloud_direct, api_key, automatic):
self.requests_total = 0
self.failures_total = 0

def lattice_fragment(self):
"""Read-only Lattice fragment: GE-Cloud devices (topology + sensor inventory)."""
from lattice import device_fragment

devices = [{"serial": str(s), "device_type": "ge-aio", "sensors": [{"capability": "soc", "unit": "%"}, {"capability": "battery_power", "unit": "W"}]} for s in getattr(self, "device_list", []) or []]
return device_fragment(devices, provider="ge-cloud", name="GivEnergy Cloud", transport="https", preference=1, locality="cloud")

async def switch_event(self, entity_id, service):
"""
Switch event
Expand Down
Loading