Skip to content

Commit c114136

Browse files
committed
feat(energy): add merge_live_status/merge_site_info wrappers
Thin key-set-bound wrappers around merge_local_into_cloud so consumers don't need to import LOCAL_LIVE_STATUS_KEYS/LOCAL_SITE_INFO_KEYS themselves. Claude-Session: https://claude.ai/code/session_0147mRrswSGt8WKikQHd8B6s
1 parent afb47e0 commit c114136

5 files changed

Lines changed: 62 additions & 11 deletions

File tree

docs/energy_local_control.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -275,30 +275,30 @@ of the cloud response's keys but fills in `None` for the ones it cannot serve
275275
locally, so a consumer that wants "local readings where available, cloud
276276
otherwise" needs to merge the two responses itself, field by field.
277277

278-
`tesla_fleet_api.router.energysite` provides that merge as a plain function,
279-
independent of `Router`:
278+
`tesla_fleet_api.router.energysite` provides that merge via two entry points,
279+
one per response shape:
280280

281281
```python
282-
from tesla_fleet_api.router.energysite import (
283-
LOCAL_LIVE_STATUS_KEYS,
284-
LOCAL_SITE_INFO_KEYS,
285-
merge_local_into_cloud,
286-
)
282+
from tesla_fleet_api.router.energysite import merge_live_status, merge_site_info
287283

288284
cloud_status = await teslemetry_energysite.live_status()
289285
local_status = await local_energysite.live_status()
290-
merged = merge_local_into_cloud(cloud_status, local_status, LOCAL_LIVE_STATUS_KEYS)
286+
merged = merge_live_status(cloud_status, local_status)
291287
```
292288

293-
- `LOCAL_LIVE_STATUS_KEYS` are the `live_status()` fields the local gateway can
294-
actually serve; `LOCAL_SITE_INFO_KEYS` are the `site_info()` equivalent
289+
- `merge_live_status` overlays the `live_status()` fields the local gateway can
290+
actually serve; `merge_site_info` does the same for `site_info()`
295291
(`backup_reserve_percent`, `default_real_mode`).
296-
- Only keys in the given set **and** present in `local` are overlaid onto a
292+
- Only keys the wrapper owns **and** present in `local` are overlaid onto a
297293
copy of `cloud`; every other key keeps its cloud value.
298294
- If the local read failed entirely (`local is None`, e.g. the LAN call
299295
raised), the result is just `cloud` - the same fallback-to-cloud behavior
300296
`Router` gives non-merged commands.
301297

298+
Both wrappers call the underlying primitive, `merge_local_into_cloud(cloud,
299+
local, owned_keys)`, which is still public for a caller with its own key set
300+
(`LOCAL_LIVE_STATUS_KEYS`/`LOCAL_SITE_INFO_KEYS` are also exported).
301+
302302
## See also
303303

304304
- [Fleet API for Energy Sites](fleet_api_energy_sites.md) - the cloud

tesla_fleet_api/router/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
LOCAL_LIVE_STATUS_KEYS,
88
LOCAL_SITE_INFO_KEYS,
99
merge_local_into_cloud,
10+
merge_live_status,
11+
merge_site_info,
1012
)
1113

1214
__all__ = [
@@ -17,4 +19,6 @@
1719
"LOCAL_LIVE_STATUS_KEYS",
1820
"LOCAL_SITE_INFO_KEYS",
1921
"merge_local_into_cloud",
22+
"merge_live_status",
23+
"merge_site_info",
2024
]

tesla_fleet_api/router/energysite.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ def merge_local_into_cloud(
4343
return merged
4444

4545

46+
def merge_live_status(cloud: dict[str, Any], local: dict[str, Any] | None) -> dict[str, Any]:
47+
"""Overlay a local Powerwall live_status onto the cloud document; see merge_local_into_cloud."""
48+
return merge_local_into_cloud(cloud, local, LOCAL_LIVE_STATUS_KEYS)
49+
50+
51+
def merge_site_info(cloud: dict[str, Any], local: dict[str, Any] | None) -> dict[str, Any]:
52+
"""Overlay a local Powerwall site_info onto the cloud document; see merge_local_into_cloud."""
53+
return merge_local_into_cloud(cloud, local, LOCAL_SITE_INFO_KEYS)
54+
55+
4656
class EnergySiteRouter(Router[PrimaryT, SecondaryT]):
4757
"""A :class:`Router` over energy-site instances.
4858

tesla_fleet_api/tesla/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
LOCAL_LIVE_STATUS_KEYS,
1414
LOCAL_SITE_INFO_KEYS,
1515
merge_local_into_cloud,
16+
merge_live_status,
17+
merge_site_info,
1618
)
1719
from tesla_fleet_api.tesla.user import User
1820
from tesla_fleet_api.tesla.vehicle import (
@@ -45,4 +47,6 @@
4547
"LOCAL_LIVE_STATUS_KEYS",
4648
"LOCAL_SITE_INFO_KEYS",
4749
"merge_local_into_cloud",
50+
"merge_live_status",
51+
"merge_site_info",
4852
]

tests/test_energysite_merge_contract.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from tesla_fleet_api.router import (
1010
LOCAL_LIVE_STATUS_KEYS,
1111
LOCAL_SITE_INFO_KEYS,
12+
merge_live_status,
1213
merge_local_into_cloud,
14+
merge_site_info,
1315
)
1416

1517

@@ -67,5 +69,36 @@ def test_site_info_keys(self) -> None:
6769
self.assertEqual(result["default_real_mode"], "self_consumption")
6870

6971

72+
class TestMergeLiveStatus(unittest.TestCase):
73+
def test_matches_generic_call_with_live_status_keys(self) -> None:
74+
cloud = {"solar_power": 100, "grid_power": 50}
75+
local = {"solar_power": 200, "some_unowned_field": "surprise"}
76+
expected = merge_local_into_cloud(cloud, local, LOCAL_LIVE_STATUS_KEYS)
77+
result = merge_live_status(cloud, local)
78+
self.assertEqual(result, expected)
79+
self.assertEqual(result["solar_power"], 200)
80+
self.assertEqual(result["grid_power"], 50)
81+
self.assertNotIn("some_unowned_field", result)
82+
83+
def test_local_none_passthrough(self) -> None:
84+
cloud = {"solar_power": 100}
85+
self.assertEqual(merge_live_status(cloud, None), cloud)
86+
87+
88+
class TestMergeSiteInfo(unittest.TestCase):
89+
def test_matches_generic_call_with_site_info_keys(self) -> None:
90+
cloud = {"backup_reserve_percent": 20, "default_real_mode": "self_consumption"}
91+
local = {"backup_reserve_percent": 30}
92+
expected = merge_local_into_cloud(cloud, local, LOCAL_SITE_INFO_KEYS)
93+
result = merge_site_info(cloud, local)
94+
self.assertEqual(result, expected)
95+
self.assertEqual(result["backup_reserve_percent"], 30)
96+
self.assertEqual(result["default_real_mode"], "self_consumption")
97+
98+
def test_local_none_passthrough(self) -> None:
99+
cloud = {"backup_reserve_percent": 20}
100+
self.assertEqual(merge_site_info(cloud, None), cloud)
101+
102+
70103
if __name__ == "__main__":
71104
unittest.main()

0 commit comments

Comments
 (0)