diff --git a/README.md b/README.md index 2a43384c..b894afc9 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,12 @@ Custom component to support Automower. ## About -The idea for this component ist coming from the integration. As this integration doesn't use the offical API, I decided to create a -integration, which is based on the offical API: . There are some disatvanteges between, the offical API and the unoffical API: - -- Offical API is limited to 10,000 accesses per 30 days. So state of the mower is only update every 5 minutes -- API-Key is needed - -But the adavantage is, that Husqvarna won't close the offical API suddenly. +This integration is based on the offical +[API](https://developer.husqvarnagroup.cloud/). The integration is using the +Husqvarna websocket API for pushed updates, so no polling is performed. You +need a API key to use this integration, refer to [this +guide](https://developer.husqvarnagroup.cloud/docs/getting-started) on how to +get one. ![Screenshot of the integration](https://github.com/Thomas55555/husqvarna_automower/blob/master/screenshot_husqvarna_automower.PNG) diff --git a/custom_components/husqvarna_automower/__init__.py b/custom_components/husqvarna_automower/__init__.py index 9199d9d6..f2ab5af0 100644 --- a/custom_components/husqvarna_automower/__init__.py +++ b/custom_components/husqvarna_automower/__init__.py @@ -1,16 +1,12 @@ """The Husqvarna Automower integration.""" import logging -from aioautomower import ( - AutomowerSession, - GetAccessToken, -) +import aioautomower from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.update_coordinator import UpdateFailed from .const import DOMAIN, PLATFORMS, STARTUP_MESSAGE @@ -28,7 +24,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry): password = entry.data.get(CONF_PASSWORD) api_key = entry.data.get(CONF_API_KEY) - get_token = GetAccessToken(api_key, username, password) + get_token = aioautomower.GetAccessToken(api_key, username, password) access_token = await get_token.async_get_access_token() hass.config_entries.async_update_entry( entry, @@ -53,10 +49,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): api_key = entry.unique_id access_token = entry.data.get(CONF_TOKEN) - session = AutomowerSession(api_key, access_token) + session = aioautomower.AutomowerSession(api_key, access_token) - if not await session.connect(): - raise ConfigEntryAuthFailed + try: + await session.connect() + except Exception as e: + # If we haven't used the refresh_token (ie. been offline) for 10 days, + # we need to login using username and password in the config flow again. + raise ConfigEntryAuthFailed(e) hass.data[DOMAIN][entry.entry_id] = session hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -65,11 +65,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): - """Handle removal of an entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + """Handle unload of an entry.""" + session = hass.data[DOMAIN].pop(entry.entry_id) + try: + await session.invalidate_token() + except Exception as exception: + _LOGGER.warning("Failed to invalidate token: %s", exception) + pass + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -80,8 +84,3 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle removal of an entry.""" - session = hass.data[DOMAIN][entry.entry_id] - try: - await session.invalidate_token() - except Exception as exception: - raise UpdateFailed(exception) from exception diff --git a/custom_components/husqvarna_automower/device_tracker.py b/custom_components/husqvarna_automower/device_tracker.py index 6eee2c41..73ce7d07 100644 --- a/custom_components/husqvarna_automower/device_tracker.py +++ b/custom_components/husqvarna_automower/device_tracker.py @@ -26,7 +26,9 @@ def __init__(self, session, idx): self.mower_name = mower_attributes["system"]["name"] self.model = mower_attributes["system"]["model"] - self.session.register_cb(lambda _: self.async_write_ha_state()) + self.session.register_cb( + lambda _: self.async_write_ha_state(), schedule_immediately=True + ) def __get_mower_attributes(self): return self.session.data["data"][self.idx]["attributes"] diff --git a/custom_components/husqvarna_automower/manifest.json b/custom_components/husqvarna_automower/manifest.json index 2c0e1ce9..3e95e605 100644 --- a/custom_components/husqvarna_automower/manifest.json +++ b/custom_components/husqvarna_automower/manifest.json @@ -11,7 +11,7 @@ "@Thomas55555" ], "requirements": [ - "aioautomower==2021.10.1" + "aioautomower==2021.10.2" ], "iot_class": "cloud_push", "version": "0.0.0" diff --git a/custom_components/husqvarna_automower/translations/en.json b/custom_components/husqvarna_automower/translations/en.json index 9c862cde..c79373e7 100644 --- a/custom_components/husqvarna_automower/translations/en.json +++ b/custom_components/husqvarna_automower/translations/en.json @@ -23,7 +23,7 @@ }, "abort": { "single_instance_allowed": "Only a single configuration of Husqvarna Automower is allowed.", - "reauth_successful": "The reauthentication was suscessfull" + "reauth_successful": "The reauthentication was successful" } }, "system_health": { diff --git a/custom_components/husqvarna_automower/vacuum.py b/custom_components/husqvarna_automower/vacuum.py index cd999544..4b6ffaae 100644 --- a/custom_components/husqvarna_automower/vacuum.py +++ b/custom_components/husqvarna_automower/vacuum.py @@ -74,7 +74,9 @@ def __init__(self, session, idx): self.model = mower_attributes["system"]["model"] self._available = None - self.session.register_cb(lambda _: self.async_write_ha_state()) + self.session.register_cb( + lambda _: self.async_write_ha_state(), schedule_immediately=True + ) def __get_mower_attributes(self): return self.session.data["data"][self.idx]["attributes"] @@ -188,9 +190,10 @@ def battery_level(self): def __get_status(self) -> str: mower_attributes = self.__get_mower_attributes() + next_start_short = "" if mower_attributes["planner"]["nextStartTimestamp"] != 0: - self.next_start_short = time.strftime( - "%a %H:%M", + next_start_short = time.strftime( + ", next start: %a %H:%M", time.gmtime((mower_attributes["planner"]["nextStartTimestamp"]) / 1000), ) if mower_attributes["mower"]["state"] == "UNKNOWN": @@ -209,7 +212,7 @@ def __get_status(self) -> str: if mower_attributes["mower"]["activity"] == "GOING_HOME": return "Going to charging station" if mower_attributes["mower"]["activity"] == "CHARGING": - return f"Charging, next start: {self.next_start_short}" + return f"Charging{next_start_short}" if mower_attributes["mower"]["activity"] == "LEAVING": return "Leaving charging station" if mower_attributes["mower"]["activity"] == "PARKED_IN_CS": @@ -222,7 +225,7 @@ def __get_status(self) -> str: return "Powering up" if mower_attributes["mower"]["state"] == "RESTRICTED": if mower_attributes["planner"]["restrictedReason"] == "WEEK_SCHEDULE": - return f"Schedule, next start: {self.next_start_short}" + return f"Schedule{next_start_short}" if mower_attributes["planner"]["restrictedReason"] == "PARK_OVERRIDE": return "Park override" if mower_attributes["planner"]["restrictedReason"] == "SENSOR":