1 """The Tesla Powerwall integration."""
3 from __future__
import annotations
5 from contextlib
import AsyncExitStack
6 from datetime
import timedelta
9 from aiohttp
import CookieJar
10 from tesla_powerwall
import (
13 MissingAttributeError,
15 PowerwallUnreachableError,
28 from .const
import DOMAIN, POWERWALL_API_CHANGED, POWERWALL_COORDINATOR, UPDATE_INTERVAL
36 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
38 _LOGGER = logging.getLogger(__name__)
40 API_CHANGED_ERROR_BODY = (
41 "It seems like your powerwall uses an unsupported version. "
42 "Please update the software of your powerwall or if it is "
43 "already the newest consider reporting this issue.\nSee logs for more information"
45 API_CHANGED_TITLE =
"Unknown powerwall software version"
49 """Class to manager powerwall data and relogin on failure."""
54 power_wall: Powerwall,
57 runtime_data: PowerwallRuntimeData,
59 """Init the data manager."""
68 """Return true if the api has changed out from under us."""
69 return self.
runtime_dataruntime_data[POWERWALL_API_CHANGED]
72 """Recreate the login on auth failure."""
73 if self.
power_wallpower_wall.is_authenticated():
78 """Fetch data from API endpoint."""
80 _LOGGER.debug(
"Checking if update failed")
86 """Fetch data from API endpoint."""
87 _LOGGER.debug(
"Updating data")
88 for attempt
in range(2):
93 except (TimeoutError, PowerwallUnreachableError)
as err:
94 raise UpdateFailed(
"Unable to fetch data from powerwall")
from err
95 except MissingAttributeError
as err:
96 _LOGGER.error(
"The powerwall api has changed: %s",
str(err))
99 persistent_notification.create(
100 self.
hasshass, API_CHANGED_ERROR_BODY, API_CHANGED_TITLE
102 self.
runtime_dataruntime_data[POWERWALL_API_CHANGED] =
True
103 raise UpdateFailed(
"The powerwall api has changed")
from err
104 except AccessDeniedError
as err:
107 raise ConfigEntryAuthFailed
from err
109 raise ConfigEntryAuthFailed
from err
110 _LOGGER.debug(
"Access denied, trying to reauthenticate")
113 except ApiError
as err:
114 raise UpdateFailed(f
"Updated failed due to {err}, will retry")
from err
117 raise RuntimeError(
"unreachable")
121 """Set up Tesla Powerwall from a config entry."""
122 ip_address: str = entry.data[CONF_IP_ADDRESS]
124 password: str |
None = entry.data.get(CONF_PASSWORD)
126 hass, verify_ssl=
False, cookie_jar=CookieJar(unsafe=
True)
129 async
with AsyncExitStack()
as stack:
130 power_wall = Powerwall(ip_address, http_session=http_session, verify_ssl=
False)
131 stack.push_async_callback(power_wall.close)
135 power_wall, ip_address, password
140 except (TimeoutError, PowerwallUnreachableError)
as err:
141 raise ConfigEntryNotReady
from err
142 except MissingAttributeError
as err:
144 _LOGGER.error(
"The powerwall api has changed: %s",
str(err))
145 persistent_notification.async_create(
146 hass, API_CHANGED_ERROR_BODY, API_CHANGED_TITLE
149 except AccessDeniedError
as err:
150 _LOGGER.debug(
"Authentication failed", exc_info=err)
151 raise ConfigEntryAuthFailed
from err
152 except ApiError
as err:
153 raise ConfigEntryNotReady
from err
155 gateway_din = base_info.gateway_din
156 if entry.unique_id
is not None and is_ip_address(entry.unique_id):
157 hass.config_entries.async_update_entry(entry, unique_id=gateway_din)
163 api_instance=power_wall,
172 name=
"Powerwall site",
173 update_method=manager.async_update_data,
174 update_interval=
timedelta(seconds=UPDATE_INTERVAL),
178 await coordinator.async_config_entry_first_refresh()
180 runtime_data[POWERWALL_COORDINATOR] = coordinator
182 entry.runtime_data = runtime_data
186 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
192 hass: HomeAssistant, entry: PowerwallConfigEntry, base_info: PowerwallBaseInfo
194 """Migrate old entity unique ids to use gateway_din."""
195 old_base_unique_id =
"_".join(base_info.serial_numbers)
196 new_base_unique_id = base_info.gateway_din
198 dev_reg = dr.async_get(hass)
199 if device := dev_reg.async_get_device(identifiers={(DOMAIN, old_base_unique_id)}):
200 dev_reg.async_update_device(
201 device.id, new_identifiers={(DOMAIN, new_base_unique_id)}
204 ent_reg = er.async_get(hass)
205 for ent_entry
in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
206 current_unique_id = ent_entry.unique_id
207 if current_unique_id.startswith(old_base_unique_id):
208 unique_id_postfix = current_unique_id.removeprefix(old_base_unique_id)
209 new_unique_id = f
"{new_base_unique_id}{unique_id_postfix}"
210 ent_reg.async_update_entity(
211 ent_entry.entity_id, new_unique_id=new_unique_id
216 power_wall: Powerwall, host: str, password: str |
None
217 ) -> PowerwallBaseInfo:
218 """Login to the powerwall and fetch the base info."""
219 if password
is not None:
220 await power_wall.login(password)
225 """Return PowerwallBaseInfo for the device."""
230 gateway_din = await power_wall.get_gateway_din()
231 site_info = await power_wall.get_site_info()
232 status = await power_wall.get_status()
233 device_type = await power_wall.get_device_type()
234 serial_numbers = await power_wall.get_serial_numbers()
235 batteries = await power_wall.get_batteries()
239 gateway_din=gateway_din,
242 device_type=device_type,
243 serial_numbers=sorted(serial_numbers),
244 url=f
"https://{host}",
245 batteries={battery.serial_number: battery
for battery
in batteries},
250 """Return the backup reserve percentage."""
252 return await power_wall.get_backup_reserve_percentage()
253 except MissingAttributeError:
258 """Process and update powerwall data."""
264 charge = await power_wall.get_charge()
265 site_master = await power_wall.get_sitemaster()
266 meters = await power_wall.get_meters()
267 grid_services_active = await power_wall.is_grid_services_active()
268 grid_status = await power_wall.get_grid_status()
269 batteries = await power_wall.get_batteries()
272 site_master=site_master,
274 grid_services_active=grid_services_active,
275 grid_status=grid_status,
276 backup_reserve=backup_reserve,
277 batteries={battery.serial_number: battery
for battery
in batteries},
283 hass: HomeAssistant, entry: PowerwallConfigEntry
285 """Return True if the last update was successful."""
287 hasattr(entry,
"runtime_data")
288 and (runtime_data := entry.runtime_data)
289 and (coordinator := runtime_data.get(POWERWALL_COORDINATOR))
290 and coordinator.last_update_success
295 """Unload a config entry."""
296 return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
PowerwallData _update_data(self)
None __init__(self, HomeAssistant hass, Powerwall power_wall, str ip_address, str|None password, PowerwallRuntimeData runtime_data)
None _recreate_powerwall_login(self)
PowerwallData async_update_data(self)
PowerwallData _fetch_powerwall_data(Powerwall power_wall)
float|None get_backup_reserve_percentage(Powerwall power_wall)
None async_migrate_entity_unique_ids(HomeAssistant hass, PowerwallConfigEntry entry, PowerwallBaseInfo base_info)
bool async_setup_entry(HomeAssistant hass, PowerwallConfigEntry entry)
PowerwallBaseInfo _login_and_fetch_base_info(Powerwall power_wall, str host, str|None password)
bool async_last_update_was_successful(HomeAssistant hass, PowerwallConfigEntry entry)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
PowerwallBaseInfo _call_base_info(Powerwall power_wall, str host)
aiohttp.ClientSession async_create_clientsession()
bool is_ip_address(str address)