Skip to content

Add Mammotion integration - #143429

Open
mikey0000 wants to merge 69 commits into
home-assistant:devfrom
mikey0000:mammotion-lawnmower
Open

Add Mammotion integration#143429
mikey0000 wants to merge 69 commits into
home-assistant:devfrom
mikey0000:mammotion-lawnmower

Conversation

@mikey0000

@mikey0000 mikey0000 commented Apr 21, 2025

Copy link
Copy Markdown
Contributor

Breaking change

Proposed change

Type of change

Add Mammotion lawn mower Integration

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies - a link to the changelog, or at minimum a diff between library versions is added to the PR description.

To help with the load of incoming pull requests:

Comment thread homeassistant/components/mammotion/diagnostics.py Outdated
Comment thread homeassistant/components/mammotion/services.yaml Outdated
Comment thread homeassistant/components/mammotion/__init__.py Outdated
Comment thread homeassistant/components/mammotion/__init__.py Outdated
Comment thread homeassistant/components/mammotion/__init__.py Outdated
Comment thread homeassistant/components/mammotion/entity.py Outdated
Comment thread homeassistant/components/mammotion/icons.json Outdated
Comment thread homeassistant/components/mammotion/lawn_mower.py Outdated
Comment thread homeassistant/components/mammotion/lawn_mower.py Outdated
Comment thread homeassistant/components/mammotion/lawn_mower.py Outdated

@NoRi2909 NoRi2909 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few quick comments on the strings from me as a translator.

Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
@github-actions

github-actions Bot commented Jul 2, 2025

Copy link
Copy Markdown

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!

@github-actions github-actions Bot added stale and removed stale labels Jul 2, 2025
@mikey0000
mikey0000 force-pushed the mammotion-lawnmower branch from 42727d1 to b35d922 Compare August 27, 2025 10:35
@emontnemery

Copy link
Copy Markdown
Contributor

@mikey0000 CI is failing

@mikey0000

Copy link
Copy Markdown
Contributor Author

@mikey0000 CI is failing

Yup I know. I recently updated this branch with more items fixed, tests also need more work. I'm onto it now.

@github-actions github-actions Bot removed the stale label Nov 2, 2025
@github-actions

github-actions Bot commented Nov 2, 2025

Copy link
Copy Markdown

There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want to keep it open. Also, please rebase your PR onto the latest dev branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!

@github-actions github-actions Bot added the stale label Nov 2, 2025
@mikey0000

Copy link
Copy Markdown
Contributor Author

Keep open

@mikey0000
mikey0000 marked this pull request as ready for review November 21, 2025 09:13

@NoRi2909 NoRi2909 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a translator only a few suggestions on the user-facing strings below.

Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
Comment thread homeassistant/components/mammotion/strings.json Outdated
@mikey0000
mikey0000 force-pushed the mammotion-lawnmower branch from ba67ae1 to e7c544c Compare December 2, 2025 22:37
@mikey0000
mikey0000 requested a review from NoRi2909 December 4, 2025 09:14
Comment thread homeassistant/components/mammotion/strings.json Outdated
@abmantis abmantis changed the title Integration: Mammotion Add Mammotion integration Dec 4, 2025
@mikey0000

Copy link
Copy Markdown
Contributor Author

All checks have passed WOOP WOOP!

@mikey0000

Copy link
Copy Markdown
Contributor Author

Going to pop this back to draft as I will do another sweep over the code.

@mikey0000
mikey0000 marked this pull request as draft January 9, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 24 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

homeassistant/components/mammotion/config.py:55

  • The docstring says this cancels the delayed write, but the implementation doesn’t cancel any scheduled async_delay_save. If the intent is to prevent a later redundant write (and avoid writes after unload), call the store’s delayed-save cancel mechanism (e.g., async_cancel_delayed_save() if available) before saving, and/or adjust _save_pending handling accordingly.
    async def async_flush_mower_data(self) -> None:
        """Write queued mower data to disk, cancelling the delayed write."""
        if not self._save_pending:
            return
        await self.async_save(self._data_to_save())

Comment thread homeassistant/components/mammotion/config_flow.py
Comment thread homeassistant/components/mammotion/config_flow.py
Comment thread homeassistant/components/mammotion/__init__.py Outdated
Copilot AI review requested due to automatic review settings August 16, 2026 04:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

homeassistant/components/mammotion/config.py:50

  • _data_to_save is documented (and in Store.async_delay_save is typically executed) in an executor thread, but it mutates _save_pending which is also read/written from the event loop thread. This introduces a cross-thread race. Prefer keeping _data_to_save side-effect-free (just return the snapshot) and clear _save_pending on the event loop thread (e.g., via an event-loop scheduled callback tied to the delayed save firing), so all state mutation happens on the same thread.
        # A pending write keeps its own deadline: async_delay_save would push the
        # write back on every call and never fire while polling continues.
        if self._save_pending:
            return
        self._save_pending = True
        self.async_delay_save(self._data_to_save, SAVE_DELAY)

    def _data_to_save(self) -> dict[str, Any]:
        """Return a snapshot to persist; runs in the executor thread."""
        self._save_pending = False
        return dict(self.mower_data)

homeassistant/components/mammotion/init.py:171

  • If device_entry.identifiers does not contain a (DOMAIN, ...) identifier, next(...) will raise StopIteration and error the device-removal flow. Use next(..., None) and handle the None case (typically by returning True to allow removal, or False if you want to block removal when the device cannot be matched).
    mower_names = (
        next(
            identifier[1]
            for identifier in device_entry.identifiers
            if identifier[0] == DOMAIN
        ),
    )

except ClientError, TimeoutError, OSError:
errors["base"] = "cannot_connect"
return errors, None

Copilot AI review requested due to automatic review settings August 16, 2026 04:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

homeassistant/components/mammotion/config.py:50

  • Store.async_delay_save() executes the provided callable in an executor thread. _data_to_save() mutates _save_pending and iterates/copies self.mower_data from that executor thread while async_update_mower_data() may be mutating it in the event loop thread. This can lead to race conditions (including RuntimeError: dictionary changed size during iteration) and incorrect _save_pending state. A safer approach is to ensure the executor callable does not touch shared mutable state: create an immutable snapshot in the event loop thread and have the executor callable only return that snapshot, or replace the delayed-save mechanism with an event-loop scheduled task/handle that calls async_save() after SAVE_DELAY.
    @callback
    def async_update_mower_data(self, device_name: str, data: dict[str, Any]) -> None:
        """Update a mower in memory, writing to disk at most once per SAVE_DELAY."""
        if self.mower_data.get(device_name) == data:
            return
        self.mower_data[device_name] = data
        # A pending write keeps its own deadline: async_delay_save would push the
        # write back on every call and never fire while polling continues.
        if self._save_pending:
            return
        self._save_pending = True
        self.async_delay_save(self._data_to_save, SAVE_DELAY)

    def _data_to_save(self) -> dict[str, Any]:
        """Return a snapshot to persist; runs in the executor thread."""
        self._save_pending = False
        return dict(self.mower_data)

homeassistant/components/mammotion/coordinator.py:11

  • NoTransportAvailableError is imported from pymammotion.transport here, while homeassistant/components/mammotion/const.py imports it from pymammotion.transport.base. If these are different symbols (or if one import path is not available in the pinned version), exception handling can silently break (e.g., not being caught by COMMAND_EXCEPTIONS or the update loop handler). Standardize the import path across the integration to a single canonical location.
from pymammotion.aliyun.exceptions import DeviceOfflineException
from pymammotion.aliyun.model.dev_by_account_response import Device
from pymammotion.data.model.device import MowingDevice
from pymammotion.homeassistant import HomeAssistantMowerApi
from pymammotion.transport import NoTransportAvailableError

homeassistant/components/mammotion/config_flow.py:37

  • This integration is opted into strict typing (.strict-typing + mypy.ini). Using self._config: dict = {} introduces an untyped dict[Any, Any] that tends to propagate Any through the flow logic. Prefer a fully typed annotation (e.g., dict[str, Any]) to keep mypy effective under strict settings.
    def __init__(self) -> None:
        """Initialize the config flow."""
        self._config: dict = {}
        self._discovered_devices: dict[str, str] = {}
        self._discovered_device: BLEDevice | None = None

"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ruff formatted it that way

Comment thread homeassistant/components/mammotion/__init__.py Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 16, 2026 04:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 3 comments.

Suppressed comments (1)

homeassistant/components/mammotion/config.py:50

  • _data_to_save is documented to run in the executor thread, but it mutates _save_pending, which is also read/written from the event loop thread in async_update_mower_data. That cross-thread mutation can race and cause missed scheduling or duplicate saves. Move the _save_pending = False transition back onto the event loop thread (e.g., clear the flag in a callback/task scheduled when the delayed save fires, and keep _data_to_save as a pure snapshot function).
    def _data_to_save(self) -> dict[str, Any]:
        """Return a snapshot to persist; runs in the executor thread."""
        self._save_pending = False
        return dict(self.mower_data)


try:
await mammotion_http.login_v2(account, password)
except ClientError, TimeoutError, OSError:
Comment thread homeassistant/components/mammotion/config_flow.py
"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:
Copilot AI review requested due to automatic review settings August 16, 2026 06:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

homeassistant/components/mammotion/config.py:46

  • self._dirty is cleared before the save completes. If async_save fails/raises (I/O error, shutdown race, etc.), pending changes will be dropped and never retried. Set _dirty = False only after a successful save (or restore it in an exception handler).
    async def async_flush_mower_data(self) -> 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))

homeassistant/components/mammotion/init.py:47

  • The ha_version value is hardcoded, which can break library compatibility/telemetry if the upstream expects the actual Home Assistant version. Prefer using the runtime version from hass.config.version (or the project’s standard version source) to keep it accurate.
    api = HomeAssistantMowerApi(
        ha_version="1.0.0", session=async_get_clientsession(hass)
    )

homeassistant/components/mammotion/init.py:192

  • This introduces async_remove_config_entry_device behavior but the added test suite doesn’t appear to cover it. Add a test that exercises device removal decisions for a Mammotion device entry vs. a non-Mammotion device entry to lock in expected UI behavior.
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)

"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:
Comment thread homeassistant/components/mammotion/__init__.py
Copilot AI review requested due to automatic review settings August 16, 2026 06:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

homeassistant/components/mammotion/init.py:127

  • Shutdown and storage flush are each scheduled in two ways: (1) via event listeners (STOP/FINAL_WRITE) and (2) via entry.async_on_unload(...). This can lead to duplicate mammotion.stop() calls and duplicate store writes during shutdown/unload, which is avoidable operational noise and can complicate debugging. Consider using only the event listeners (for HA stop) and only the unload callbacks (for entry unload), or gating the scheduled tasks to ensure they run once.
    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, schedule_flush_mower_data
        )
    )

    entry.async_on_unload(schedule_shutdown_mammotion)
    entry.async_on_unload(schedule_flush_mower_data)

homeassistant/components/mammotion/init.py:48

  • The ha_version is hard-coded to \"1.0.0\", which is likely incorrect and may affect upstream behavior (e.g., user-agent/version-based logic). Prefer using Home Assistant's actual runtime version (or a constant provided by HA) so the library receives accurate version info.
    api = HomeAssistantMowerApi(
        ha_version="1.0.0", session=async_get_clientsession(hass)
    )


try:
await mammotion_http.login_v2(account, password)
except ClientError, TimeoutError, OSError:
"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:
@mikey0000
mikey0000 marked this pull request as ready for review August 16, 2026 07:14
Copilot AI review requested due to automatic review settings August 16, 2026 08:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 4 comments.

Suppressed comments (3)

homeassistant/components/mammotion/coordinator.py:11

  • The import path for NoTransportAvailableError differs from homeassistant/components/mammotion/const.py (which imports it from pymammotion.transport.base). Standardize on a single import location across the integration to reduce the risk of catching/raising different exception classes and to keep exception handling consistent.
from pymammotion.transport import NoTransportAvailableError

homeassistant/components/mammotion/coordinator.py:106

  • Calling from_dict via an instance (MowingDevice().from_dict(...)) is unconventional and makes it harder to tell whether it’s a classmethod or mutating instance method. Prefer the explicit form (MowingDevice.from_dict(mower_data)) to clarify intent and avoid confusion during future refactors.
        mower_state = MowingDevice()
        if mower_data := self.store.mower_data.get(self.device_name):
            try:
                mower_state = MowingDevice().from_dict(mower_data)
            except InvalidFieldValue:
                mower_state = MowingDevice()

homeassistant/components/mammotion/coordinator.py:76

  • CommandFailedError is a HomeAssistantError, but this raise uses a raw, non-translated message even though the integration defines exception translations in strings.json. Consider raising with translation_domain/translation_key (or only raising translated HomeAssistantError at the service layer) so user-facing failures remain localizable and consistent.
        if not await self.api.async_send_command(self.device_name, command, **kwargs):
            raise CommandFailedError(f"Command {command} failed for {self.device_name}")


return errors, login_info.userInformation.userAccount

async def async_step_wifi(
"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:
Comment on lines +221 to +233
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE

mock_mower_api.update.return_value = mock_mowing_device
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE

mock_mower_api.is_online.return_value = False
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
Comment thread tests/components/mammotion/test_init.py
Copilot AI review requested due to automatic review settings August 16, 2026 08:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

homeassistant/components/mammotion/config_flow.py:236

  • user_account is typed as str | None from _async_validate_login, but is passed to async_set_unique_id without narrowing. Even if logically non-None when errors is empty, this will fail strict typing and is fragile if _async_validate_login changes. Add an explicit narrowing step (e.g., assert user_account is not None or restructure _async_validate_login to return str on success) before calling async_set_unique_id.
            account = user_input[CONF_ACCOUNTNAME]
            password = user_input[CONF_PASSWORD]
            errors, user_account = await self._async_validate_login(account, password)

            if not errors:
                await self.async_set_unique_id(user_account, raise_on_progress=False)
                self._abort_if_unique_id_configured()

                return self.async_create_entry(

Comment on lines +117 to +123
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:
return self.data

if data is None:
raise UpdateFailed(f"No data returned for {self.device_name}")
Comment thread homeassistant/components/mammotion/__init__.py Outdated
Copilot AI review requested due to automatic review settings August 16, 2026 09:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (5)

homeassistant/components/mammotion/config_flow.py:141

  • The dict merge order here prevents updating an existing entry’s stored BLE address for the same mower name: **entry.data[...] will overwrite the newly-discovered name: ... value. Swap the merge order so the freshly-discovered address wins. Consider adding a regression test that starts with an entry having CONF_BLE_DEVICES for name and confirms a new advertisement updates it.
        if entry := await self.check_and_update_bluetooth_device(device):
            existing_devices = {
                name: format_mac(device.address),
                **entry.data.get(CONF_BLE_DEVICES, {}),
            }
            self._abort_if_unique_id_configured(
                updates={CONF_BLE_DEVICES: existing_devices}, reload_on_update=False
            )

homeassistant/components/mammotion/config.py:45

  • The dirty flag is cleared before the save completes. If async_save(...) raises (I/O error, disk full, etc.), pending state is silently dropped and won’t be retried. Set _dirty = False only after a successful save (or restore _dirty = True in an exception handler).
    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))

homeassistant/components/mammotion/lawn_mower.py:95

  • activity treats both WorkMode.MODE_PAUSE and WorkMode.MODE_CHARGING_PAUSE as paused, but async_start_mowing only resumes when MODE_PAUSE. If the device reports MODE_CHARGING_PAUSE, SERVICE_START_MOWING will currently send start_job instead of resume_execute_task. Include MODE_CHARGING_PAUSE in the resume condition to match the activity mapping.
        if mode == WorkMode.MODE_PAUSE:
            trans_key = "resume_failed"
            try:
                await self.coordinator.async_send_command("resume_execute_task")

homeassistant/components/mammotion/init.py:44

  • Hard-coding ha_version=\"1.0.0\" will become stale and may affect library behavior (e.g., user-agent/version gating). Prefer passing Home Assistant’s actual version (e.g., via the core version constant) or omit it if the library can derive a sensible default.
    api = HomeAssistantMowerApi(
        ha_version="1.0.0", session=async_get_clientsession(hass)
    )

homeassistant/components/mammotion/coordinator.py:11

  • Within this PR, NoTransportAvailableError is imported from different modules (pymammotion.transport here vs pymammotion.transport.base in const.py). Align these imports to a single canonical location to avoid confusion and reduce the chance of runtime import issues if the library reorganizes symbols.
from pymammotion.homeassistant import HomeAssistantMowerApi
from pymammotion.transport import NoTransportAvailableError

"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:

try:
await mammotion_http.login_v2(account, password)
except ClientError, TimeoutError, OSError:
Copilot AI review requested due to automatic review settings August 16, 2026 09:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

homeassistant/components/mammotion/init.py:164

  • There is duplicated credential-persistence logic here and in MammotionBaseUpdateCoordinator.store_cloud_credentials(). Consolidating into a single helper (or using one implementation from the other) reduces drift risk and makes future credential schema changes safer.
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}
    )

homeassistant/components/mammotion/init.py:188

  • If the entry is loaded but entry.runtime_data.mowers is empty (e.g., account has no supported devices), unload will skip mammotion.stop() and state flush. This can leak an active client/session across unload/reload cycles. Consider always stopping the client and flushing the store when unload_ok is true (you may need to store api/store in runtime_data so it’s available even with zero mowers).
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

data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:
return self.data


try:
await mammotion_http.login_v2(account, password)
except ClientError, TimeoutError, OSError:
Copilot AI review requested due to automatic review settings August 16, 2026 09:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

homeassistant/components/mammotion/config_flow.py:141

  • The dict merge order here prevents the newly discovered address from updating an existing CONF_BLE_DEVICES entry when the mower name key already exists (the existing entry value overrides the new one). Swap the merge order so the discovered {name: format_mac(device.address)} wins.
            existing_devices = {
                name: format_mac(device.address),
                **entry.data.get(CONF_BLE_DEVICES, {}),
            }
            self._abort_if_unique_id_configured(
                updates={CONF_BLE_DEVICES: existing_devices}, reload_on_update=False
            )

homeassistant/components/mammotion/init.py:53

  • Hard-coding ha_version=\"1.0.0\" is likely incorrect and can lead to misleading client identification (and harder debugging). Prefer using Home Assistant’s actual runtime version (e.g., homeassistant.const.__version__) or a value derived from hass.
    api = HomeAssistantMowerApi(
        ha_version="1.0.0", session=async_get_clientsession(hass)
    )

homeassistant/components/mammotion/lawn_mower.py:118

  • This duplicates the same error wrapping and IoT sync logic across both branches. Consider consolidating the try/except/finally so only the command + translation key selection differs; this reduces the chance of future drift (e.g., one branch forgetting async_request_iot_sync).
        if mode == WorkMode.MODE_PAUSE:
            trans_key = "resume_failed"
            try:
                await self.coordinator.async_send_command("resume_execute_task")
            except COMMAND_EXCEPTIONS as exc:
                raise HomeAssistantError(
                    translation_domain=DOMAIN, translation_key=trans_key
                ) from exc
            finally:
                await self.coordinator.api.async_request_iot_sync(
                    self.coordinator.device_name
                )

        else:
            try:
                await self.coordinator.async_send_command("start_job")
            except COMMAND_EXCEPTIONS as exc:
                raise HomeAssistantError(
                    translation_domain=DOMAIN, translation_key=trans_key
                ) from exc
            finally:
                await self.coordinator.api.async_request_iot_sync(
                    self.coordinator.device_name
                )

"""Get data from the device."""
try:
data = await self.api.update(self.device_name)
except DeviceOfflineException, NoTransportAvailableError:

try:
await mammotion_http.login_v2(account, password)
except ClientError, TimeoutError, OSError:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants