5757
5858OPERATION_MODES = ["self_consumption" , "autonomous" , "backup" ]
5959EXPORT_RULES = ["never" , "pv_only" , "battery_ok" ]
60+ # Large nested tariff structures that are never worth republishing - hidden from both the debug log
61+ # summary and the site_info review entity.
62+ TARIFF_BLOB_KEYS = ("tariff_content" , "tariff_content_v2" )
6063# tou_settings.optimization_strategy: the OTHER dial the Fleet API exposes alongside the tariff itself,
6164# deciding whether Time-Based Control actually acts on price. "balanced" (the device default, and what
6265# is left in place if this is never sent) only discharges to offset house load and never exports stored
@@ -240,7 +243,7 @@ def _summarize_for_log(data):
240243 for key in ("time_series" , "SmartBreakerEnergyLogs" ):
241244 if isinstance (response .get (key ), list ):
242245 response [key ] = "[{} entries hidden]" .format (len (response [key ]))
243- for key in ( "tariff_content" , "tariff_content_v2" ) :
246+ for key in TARIFF_BLOB_KEYS :
244247 if isinstance (response .get (key ), dict ):
245248 response [key ] = "[hidden, code={}]" .format (response [key ].get ("code" ))
246249 return {"response" : response }
@@ -252,6 +255,21 @@ def publish_sensor(self, suffix, state, unit=None, state_class="measurement", fr
252255 attributes ["unit_of_measurement" ] = unit
253256 self .dashboard_item (self .entity (suffix ), state , attributes , app = "teslemetry" )
254257
258+ def publish_site_info (self , response ):
259+ """Publish the site_info response as one entity so the device's own view of the site is reviewable.
260+
261+ Capacity, AC rating, battery coupling and the export rule all come from here and all change how
262+ Predbat models the site, but none of it was visible outside a debug log line. The whole response
263+ is published rather than a hand-picked subset, so fields added by future firmware appear without
264+ a code change - and so nothing is lost to a wrong guess about where a field is nested (batteries,
265+ customer_preferred_export_rule and net_meter_mode all live under components, not at the top).
266+ Only the tariff blobs are dropped, the same large nested structures _summarize_for_log hides.
267+ """
268+ attributes = {key : value for key , value in response .items () if key not in TARIFF_BLOB_KEYS }
269+ attributes ["friendly_name" ] = "Powerwall Site Info"
270+ attributes ["state_class" ] = None
271+ self .dashboard_item (self .entity ("site_info" ), response .get ("site_name" ) or "unknown" , attributes , app = "teslemetry" )
272+
255273 def publish_soc_max (self , kwh , estimate = False ):
256274 """Publish the battery capacity (soc_max) in kWh, preferring a real device value over an estimate.
257275
@@ -322,6 +340,7 @@ async def fetch_site_info(self):
322340 if not data :
323341 return False
324342 response = data .get ("response" , {})
343+ self .publish_site_info (response )
325344 nameplate_wh = response .get ("nameplate_energy" , 0 )
326345 battery_count = response .get ("battery_count" )
327346 if nameplate_wh :
@@ -634,6 +653,9 @@ async def automatic_config(self):
634653 max_site_meter_power_ac, so wiring them unconditionally would point Predbat at entities
635654 that never exist on a site missing those fields. Predbat falls back to its own defaults
636655 for absent args, so skipping the wiring here is safe.
656+
657+ Unlike the args above, inverter_hybrid is one of Predbat's OWN config switches rather than a
658+ component entity, so it is written through set_state_external (see below).
637659 """
638660 self .log ("Info: Teslemetry automatic configuration - wiring Predbat to the TESLA inverter type" )
639661 self .set_arg ("inverter_type" , ["TESLA" ])
@@ -664,6 +686,16 @@ async def automatic_config(self):
664686 self .set_arg ("discharge_target_soc" , [self .entity ("schedule_discharge_soc" , domain = "number" )])
665687 self .set_arg ("scheduled_discharge_enable" , [self .entity ("schedule_discharge_enable" , domain = "switch" )])
666688 self .set_arg ("schedule_write_button" , [self .entity ("schedule_write" , domain = "switch" )])
689+ # Every Powerwall is an AC-coupled battery, so Predbat must not model it as a hybrid. Left at
690+ # Predbat's default (on), get_total_inverted() folds PV into the inverter_limit budget, so the
691+ # Powerwall's own AC rating is applied as a cap on battery + PV combined - modelling a
692+ # separately inverted solar array as clipping against a limit it never passes through, which
693+ # invents both the clipping and the export windows that "recover" it.
694+ # set_state_external is the write path that updates the matching CONFIG_ITEMS value; a plain
695+ # state write would move the entity without changing the setting Predbat plans with.
696+ hybrid_entity = "switch.{}_inverter_hybrid" .format (self .prefix )
697+ self .log ("Info: Teslemetry setting {} off - Tesla Powerwall batteries are AC coupled" .format (hybrid_entity ))
698+ await self .set_state_external (hybrid_entity , False )
667699
668700 async def schedule_event (self , entity_id , value ):
669701 """Stage a schedule entity write into pending_schedule; the write switch commits it.
0 commit comments