-
-
Notifications
You must be signed in to change notification settings - Fork 38.4k
Add Mammotion integration #143429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mikey0000
wants to merge
69
commits into
home-assistant:dev
Choose a base branch
from
mikey0000:mammotion-lawnmower
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,642
−0
Open
Add Mammotion integration #143429
Changes from all commits
Commits
Show all changes
69 commits
Select commit
Hold shift + click to select a range
6e653a9
initial start of HA mammotion
mikey0000 a4c2d29
tie the coordinator to init
mikey0000 3b6a74a
tidy up and fix a few more things
mikey0000 8373f28
fix config flow and other small issues
mikey0000 76693d4
further fixes that make activity work
mikey0000 076d449
tmp
mikey0000 43765ea
development mammotion
mikey0000 88fd516
further updates
mikey0000 d0672c3
further work
mikey0000 5763131
lots of changes
mikey0000 28f3ada
update core to hacs version
mikey0000 1cf44a0
migrate over hacs updates
mikey0000 a5ec6a7
update mammotion code
mikey0000 6d67bfb
further work to fix up various issues
mikey0000 ccfd579
initially working now
mikey0000 a6c18e8
few more tweaks so timeouts get retried and we now have online / offl…
mikey0000 35da0fd
added map coordinator and start sorting bluetooth
mikey0000 bfb5006
fix entity ids
mikey0000 712bab2
minor tweaks so we actually save the config
mikey0000 1cb4253
mammotion changes, still need to re-update it again
mikey0000 0cf22e8
prepare for merging to core
mikey0000 2b45839
updates from PR comments, change strings, fix a few strings, remove s…
mikey0000 bb2e384
fix Bluetooth string
mikey0000 0d03550
remove unused device
mikey0000 6d1e048
additions from HACS
mikey0000 0b07aad
update the mammotion integration based off hacs for core
mikey0000 21a53fc
more work towards readiness
mikey0000 a995a28
update dependency and re-add bluetooth dependency
mikey0000 e432c80
update to the new library, remove all other coordinators, fix tests
mikey0000 b06f0a8
fix generated files
mikey0000 2c0ff57
formatting and add quality scale yaml
mikey0000 aa1a57a
fix pylint warnings and errors
mikey0000 d4fdbbf
remove if check
mikey0000 96ed80e
Apply suggestions from code review
mikey0000 dd68a3c
update test lawn mower
mikey0000 84b8cea
further fixes to tests and config
mikey0000 d5ecb2d
change coordinator to be fixed to return MowerDevice
mikey0000 653ec96
fix bad if statement
mikey0000 1abeb4b
update requirements and fix codeowners
mikey0000 9729e23
meet the quality scale requirements
mikey0000 eb10e76
update strings
mikey0000 54c9552
Update homeassistant/components/mammotion/strings.json
mikey0000 6f881e0
update version of pymammotion
mikey0000 5107a0a
update config flow to fix issues around device discovery
mikey0000 2969987
stop mammotion on shutdown, bump library
mikey0000 36dc5e3
remove device limits from HA and move to the api
mikey0000 1ec413e
Update tests and config flow to remove bluetooth options
mikey0000 84ed6ff
update pymammotion
mikey0000 2c986a7
fix step bluetooth signature
mikey0000 198e6cc
fix up translation key
mikey0000 61b5c68
update the integration with the newest changes from the library and hacs
mikey0000 ec23e4d
quality scale missing items and a couple exception items
mikey0000 884771e
try sort out codeowners
mikey0000 671b82c
Merge branch 'dev' into mammotion-lawnmower
mikey0000 99c5efc
address copilot issues
mikey0000 cfe01bf
Potential fix for pull request finding
mikey0000 57d6e62
bump pymammotion
mikey0000 4045f86
small tweak to docked logic and library bump
mikey0000 eff1ad7
Merge branch 'dev' into mammotion-lawnmower
mikey0000 93ef7f8
addressing more comments
mikey0000 140af6c
Merge branch 'dev' into mammotion-lawnmower
mikey0000 f7ff33e
addressing copilot issues
mikey0000 c36b6f2
handle transient states which are ok, correctly fail on login failure
mikey0000 cbddf3b
Potential fix for pull request finding
mikey0000 4844742
change write behaviour to when home assistant shuts down / stops or r…
mikey0000 c5411c8
fix call
mikey0000 dee194c
fix issue with reload causing two clients to be active
mikey0000 1a08a49
shift unload operations to unload entry
mikey0000 fab3fcd
few more minor changes
mikey0000 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| """The Mammotion integration.""" | ||
|
|
||
| import contextlib | ||
| from typing import Any | ||
|
|
||
| from aiohttp import ClientConnectorError | ||
| from pymammotion.aliyun.model.dev_by_account_response import Device | ||
| from pymammotion.client import MammotionClient | ||
| from pymammotion.homeassistant import HomeAssistantMowerApi | ||
| from pymammotion.transport.base import LoginFailedError, TransportType | ||
| from Tea.exceptions import UnretryableException | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( | ||
| CONF_PASSWORD, | ||
| EVENT_HOMEASSISTANT_FINAL_WRITE, | ||
| EVENT_HOMEASSISTANT_STOP, | ||
| Platform, | ||
| ) | ||
| from homeassistant.core import Event, HomeAssistant | ||
| from homeassistant.exceptions import ( | ||
| ConfigEntryAuthFailed, | ||
| ConfigEntryError, | ||
| ConfigEntryNotReady, | ||
| ) | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
| from homeassistant.helpers.device_registry import DeviceEntry | ||
|
|
||
| from .config import MammotionConfigStore | ||
| from .const import ( | ||
| CONF_ACCOUNTNAME, | ||
| CONF_AEP_DATA, | ||
| CONF_MAMMOTION_DEVICE_RECORDS, | ||
| CONF_MAMMOTION_MQTT, | ||
| DEVICE_SUPPORT, | ||
| DOMAIN, | ||
| EXPIRED_CREDENTIAL_EXCEPTIONS, | ||
| LOGGER, | ||
| ) | ||
| from .coordinator import MammotionMowerUpdateCoordinator | ||
| from .models import MammotionDevices, MammotionMowerData | ||
|
|
||
| PLATFORMS: list[Platform] = [Platform.LAWN_MOWER] | ||
|
|
||
| type MammotionConfigEntry = ConfigEntry[MammotionDevices] | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: MammotionConfigEntry) -> bool: | ||
| """Set up Mammotion Luba from a config entry.""" | ||
|
|
||
| api = HomeAssistantMowerApi( | ||
| ha_version="1.0.0", session=async_get_clientsession(hass) | ||
| ) | ||
| mammotion = api.mammotion | ||
| account = entry.data.get(CONF_ACCOUNTNAME) | ||
| password = entry.data.get(CONF_PASSWORD) | ||
|
|
||
| mammotion_mowers: list[MammotionMowerData] = [] | ||
| mammotion_devices: MammotionDevices = MammotionDevices([]) | ||
| store = MammotionConfigStore(hass, entry.entry_id) | ||
| await store.async_load_mower_data() | ||
|
|
||
| if account and password: | ||
| session = async_get_clientsession(hass) | ||
| cached = _load_cached_credentials(entry) | ||
|
|
||
| async def credentials_updated() -> None: | ||
| """Persist credentials the library rotated behind our back.""" | ||
| store_cloud_credentials(hass, entry, mammotion) | ||
|
|
||
| async def unrecoverable_auth_error( | ||
| account_id: str, transport_type: TransportType, err: Exception | ||
| ) -> None: | ||
| """Surface a dead cloud session as a single reauth prompt.""" | ||
| LOGGER.error( | ||
| "Mammotion cloud authentication failed for %s on %s: %s", | ||
| account_id, | ||
| transport_type.value, | ||
| err, | ||
| ) | ||
| entry.async_start_reauth(hass) | ||
|
|
||
| # Wired before login so a rotation during it is not lost. | ||
| mammotion.on_credentials_updated = credentials_updated | ||
| mammotion.on_unrecoverable_auth_error = unrecoverable_auth_error | ||
|
|
||
| try: | ||
| if cached: | ||
| try: | ||
| await mammotion.restore_credentials( | ||
| account, password, cached, session | ||
| ) | ||
| except EXPIRED_CREDENTIAL_EXCEPTIONS: | ||
| await mammotion.login_and_initiate_cloud(account, password, session) | ||
| else: | ||
| await mammotion.login_and_initiate_cloud(account, password, session) | ||
| except ClientConnectorError as err: | ||
| raise ConfigEntryNotReady(err) from err | ||
| except LoginFailedError as err: | ||
| raise ConfigEntryAuthFailed(err) from err | ||
| except UnretryableException as err: | ||
| raise ConfigEntryError(err) from err | ||
|
|
||
| store_cloud_credentials(hass, entry, mammotion) | ||
|
|
||
| device_list: list[Device] = [ | ||
| device | ||
| for device in ( | ||
| *mammotion.aliyun_device_list, | ||
| *mammotion.mammotion_device_list, | ||
| ) | ||
| if device.device_name.startswith(DEVICE_SUPPORT) | ||
| ] | ||
|
|
||
| for device in device_list: | ||
| update_coordinator = MammotionMowerUpdateCoordinator( | ||
| hass, entry, device, api, store | ||
| ) | ||
|
|
||
| update_coordinator.restore_data() | ||
| await update_coordinator.async_config_entry_first_refresh() | ||
|
|
||
| mammotion_mowers.append( | ||
| MammotionMowerData( | ||
| name=device.device_name, | ||
| api=api, | ||
| coordinator=update_coordinator, | ||
| device=device, | ||
| ) | ||
| ) | ||
|
|
||
| mammotion_devices.mowers = mammotion_mowers | ||
| entry.runtime_data = mammotion_devices | ||
|
|
||
| async def shutdown_mammotion(_: Event | None = None) -> None: | ||
| await mammotion.stop() | ||
|
|
||
| entry.async_on_unload( | ||
| hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_mammotion) | ||
| ) | ||
|
|
||
| entry.async_on_unload( | ||
| hass.bus.async_listen_once( | ||
| EVENT_HOMEASSISTANT_FINAL_WRITE, store.async_flush_mower_data | ||
| ) | ||
| ) | ||
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def store_cloud_credentials( | ||
| hass: HomeAssistant, | ||
| config_entry: MammotionConfigEntry, | ||
| mammotion: MammotionClient, | ||
| ) -> None: | ||
| """Store cloud credentials in config entry.""" | ||
| cache = mammotion.to_cache() | ||
| if not cache: | ||
| return | ||
| hass.config_entries.async_update_entry( | ||
| config_entry, data={**config_entry.data, **cache} | ||
| ) | ||
|
|
||
|
|
||
| def _load_cached_credentials(entry: MammotionConfigEntry) -> dict[str, Any]: | ||
| """Return the config entry's cached credential data, keyed as the library expects.""" | ||
| has_aliyun = bool(entry.data.get(CONF_AEP_DATA)) | ||
| has_mammotion = bool(entry.data.get(CONF_MAMMOTION_MQTT)) and bool( | ||
| entry.data.get(CONF_MAMMOTION_DEVICE_RECORDS) | ||
| ) | ||
| return dict(entry.data) if (has_aliyun or has_mammotion) else {} | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: MammotionConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
|
|
||
| if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
| for mower in entry.runtime_data.mowers: | ||
| mower.coordinator.store_cloud_credentials() | ||
| with contextlib.suppress(TimeoutError): | ||
| await mower.api.mammotion.remove_device(mower.name) | ||
|
|
||
| if mowers := entry.runtime_data.mowers: | ||
| await mowers[0].coordinator.store.async_flush_mower_data() | ||
| await mowers[0].api.mammotion.stop() | ||
| return unload_ok | ||
|
|
||
|
|
||
| async def async_remove_entry(hass: HomeAssistant, entry: MammotionConfigEntry) -> None: | ||
| """Remove a config entry.""" | ||
| store = MammotionConfigStore(hass, entry.entry_id) | ||
| await store.async_remove() | ||
|
|
||
|
|
||
| async def async_remove_config_entry_device( | ||
| hass: HomeAssistant, config_entry: MammotionConfigEntry, device_entry: DeviceEntry | ||
| ) -> bool: | ||
| """Remove a config entry from a device.""" | ||
| mower_names = ( | ||
| next( | ||
| identifier[1] | ||
| for identifier in device_entry.identifiers | ||
| if identifier[0] == DOMAIN | ||
| ), | ||
| ) | ||
| mower = next( | ||
| ( | ||
| mower | ||
| for mower in config_entry.runtime_data.mowers | ||
| if mower.name in mower_names | ||
| ), | ||
| None, | ||
| ) | ||
|
|
||
| return not bool(mower) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| """Config storage for Mammotion integration.""" | ||
|
|
||
| from typing import Any | ||
|
|
||
| from homeassistant.core import Event, HomeAssistant, callback | ||
| from homeassistant.helpers.storage import Store | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
| STORAGE_VERSION = 1 | ||
| STORAGE_MINOR_VERSION = 0 | ||
|
|
||
|
|
||
| class MammotionConfigStore(Store[dict[str, Any]]): | ||
| """Store the mower state of a single config entry.""" | ||
|
|
||
| def __init__(self, hass: HomeAssistant, entry_id: str) -> None: | ||
| """Initialize the configuration store.""" | ||
| super().__init__( | ||
| hass, | ||
| version=STORAGE_VERSION, | ||
| minor_version=STORAGE_MINOR_VERSION, | ||
| key=f"{DOMAIN}.{entry_id}", | ||
| ) | ||
| self.mower_data: dict[str, Any] = {} | ||
| self._dirty = False | ||
|
|
||
| async def async_load_mower_data(self) -> None: | ||
| """Load the persisted mower data into memory.""" | ||
| self.mower_data = await self.async_load() or {} | ||
|
|
||
| @callback | ||
| def async_update_mower_data(self, device_name: str, data: dict[str, Any]) -> None: | ||
| """Update a mower in memory; the data is only written on shutdown.""" | ||
| if self.mower_data.get(device_name) == data: | ||
| return | ||
| self.mower_data[device_name] = data | ||
| self._dirty = True | ||
|
|
||
| async def async_flush_mower_data(self, _event: Event | None = None) -> None: | ||
| """Write the in-memory mower data to disk if it changed.""" | ||
| if not self._dirty: | ||
| return | ||
| self._dirty = False | ||
| await self.async_save(dict(self.mower_data)) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.