Skip to content
Open
Show file tree
Hide file tree
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 May 15, 2024
a4c2d29
tie the coordinator to init
mikey0000 May 17, 2024
3b6a74a
tidy up and fix a few more things
mikey0000 May 18, 2024
8373f28
fix config flow and other small issues
mikey0000 May 19, 2024
76693d4
further fixes that make activity work
mikey0000 May 24, 2024
076d449
tmp
mikey0000 Aug 11, 2024
43765ea
development mammotion
mikey0000 Aug 19, 2024
88fd516
further updates
mikey0000 Aug 23, 2024
d0672c3
further work
mikey0000 Aug 26, 2024
5763131
lots of changes
mikey0000 Aug 29, 2024
28f3ada
update core to hacs version
mikey0000 Sep 2, 2024
1cf44a0
migrate over hacs updates
mikey0000 Oct 23, 2024
a5ec6a7
update mammotion code
mikey0000 Oct 30, 2024
6d67bfb
further work to fix up various issues
mikey0000 Feb 2, 2025
ccfd579
initially working now
mikey0000 Feb 16, 2025
a6c18e8
few more tweaks so timeouts get retried and we now have online / offl…
mikey0000 Feb 19, 2025
35da0fd
added map coordinator and start sorting bluetooth
mikey0000 Feb 24, 2025
bfb5006
fix entity ids
mikey0000 Feb 26, 2025
712bab2
minor tweaks so we actually save the config
mikey0000 Feb 27, 2025
1cb4253
mammotion changes, still need to re-update it again
mikey0000 Mar 22, 2025
0cf22e8
prepare for merging to core
mikey0000 Apr 21, 2025
2b45839
updates from PR comments, change strings, fix a few strings, remove s…
mikey0000 Apr 28, 2025
bb2e384
fix Bluetooth string
mikey0000 May 3, 2025
0d03550
remove unused device
mikey0000 May 3, 2025
6d1e048
additions from HACS
mikey0000 Jun 17, 2025
0b07aad
update the mammotion integration based off hacs for core
mikey0000 Aug 12, 2025
21a53fc
more work towards readiness
mikey0000 Aug 27, 2025
a995a28
update dependency and re-add bluetooth dependency
mikey0000 Aug 28, 2025
e432c80
update to the new library, remove all other coordinators, fix tests
mikey0000 Nov 19, 2025
b06f0a8
fix generated files
mikey0000 Nov 19, 2025
2c0ff57
formatting and add quality scale yaml
mikey0000 Nov 19, 2025
aa1a57a
fix pylint warnings and errors
mikey0000 Nov 21, 2025
d4fdbbf
remove if check
mikey0000 Nov 21, 2025
96ed80e
Apply suggestions from code review
mikey0000 Nov 21, 2025
dd68a3c
update test lawn mower
mikey0000 Nov 23, 2025
84b8cea
further fixes to tests and config
mikey0000 Dec 2, 2025
d5ecb2d
change coordinator to be fixed to return MowerDevice
mikey0000 Dec 3, 2025
653ec96
fix bad if statement
mikey0000 Dec 3, 2025
1abeb4b
update requirements and fix codeowners
mikey0000 Dec 4, 2025
9729e23
meet the quality scale requirements
mikey0000 Dec 4, 2025
eb10e76
update strings
mikey0000 Dec 4, 2025
54c9552
Update homeassistant/components/mammotion/strings.json
mikey0000 Dec 4, 2025
6f881e0
update version of pymammotion
mikey0000 Dec 4, 2025
5107a0a
update config flow to fix issues around device discovery
mikey0000 Dec 5, 2025
2969987
stop mammotion on shutdown, bump library
mikey0000 Dec 30, 2025
36dc5e3
remove device limits from HA and move to the api
mikey0000 Jan 2, 2026
1ec413e
Update tests and config flow to remove bluetooth options
mikey0000 Jan 23, 2026
84ed6ff
update pymammotion
mikey0000 Jul 3, 2026
2c986a7
fix step bluetooth signature
mikey0000 Jul 3, 2026
198e6cc
fix up translation key
mikey0000 Jul 3, 2026
61b5c68
update the integration with the newest changes from the library and hacs
mikey0000 Jul 3, 2026
ec23e4d
quality scale missing items and a couple exception items
mikey0000 Jul 3, 2026
884771e
try sort out codeowners
mikey0000 Jul 4, 2026
671b82c
Merge branch 'dev' into mammotion-lawnmower
mikey0000 Jul 14, 2026
99c5efc
address copilot issues
mikey0000 Jul 14, 2026
cfe01bf
Potential fix for pull request finding
mikey0000 Jul 16, 2026
57d6e62
bump pymammotion
mikey0000 Jul 16, 2026
4045f86
small tweak to docked logic and library bump
mikey0000 Aug 14, 2026
eff1ad7
Merge branch 'dev' into mammotion-lawnmower
mikey0000 Aug 14, 2026
93ef7f8
addressing more comments
mikey0000 Aug 14, 2026
140af6c
Merge branch 'dev' into mammotion-lawnmower
mikey0000 Aug 16, 2026
f7ff33e
addressing copilot issues
mikey0000 Aug 16, 2026
c36b6f2
handle transient states which are ok, correctly fail on login failure
mikey0000 Aug 16, 2026
cbddf3b
Potential fix for pull request finding
mikey0000 Aug 16, 2026
4844742
change write behaviour to when home assistant shuts down / stops or r…
mikey0000 Aug 16, 2026
c5411c8
fix call
mikey0000 Aug 16, 2026
dee194c
fix issue with reload causing two clients to be active
mikey0000 Aug 16, 2026
1a08a49
shift unload operations to unload entry
mikey0000 Aug 16, 2026
fab3fcd
few more minor changes
mikey0000 Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ homeassistant.components.luftdaten.*
homeassistant.components.lunatone.*
homeassistant.components.lutron.*
homeassistant.components.madvr.*
homeassistant.components.mammotion.*
homeassistant.components.manual.*
homeassistant.components.marantz_infrared.*
homeassistant.components.mastodon.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

217 changes: 217 additions & 0 deletions homeassistant/components/mammotion/__init__.py
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(
Comment thread
mikey0000 marked this conversation as resolved.
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)
45 changes: 45 additions & 0 deletions homeassistant/components/mammotion/config.py
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))
Loading
Loading