-
-
Notifications
You must be signed in to change notification settings - Fork 38.5k
Add Mitsubishi WF-RAC integration #181403
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
blues-sechseck
wants to merge
6
commits into
home-assistant:dev
Choose a base branch
from
blues-sechseck:mitsubishi-wf-rac-initial
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.
Open
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
a79b714
Add Mitsubishi WF-RAC integration
blues-sechseck 643bbb2
Address review feedback
blues-sechseck 09de092
Let actions issued together share one frame again
blues-sechseck cee8de4
Take the last of the pylint findings
blues-sechseck ea18e76
Cover what the tests were only asserting around
blues-sechseck 32c962c
Take the review's remaining findings
blues-sechseck 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
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,242 @@ | ||
| """The Mitsubishi WF-RAC integration.""" | ||
|
|
||
| from dataclasses import dataclass | ||
| import logging | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( | ||
| CONF_DEVICE_ID, | ||
| CONF_HOST, | ||
| CONF_NAME, | ||
| CONF_PORT, | ||
| Platform, | ||
| ) | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import ConfigEntryNotReady | ||
| from homeassistant.helpers import issue_registry as ir | ||
|
|
||
| from .const import ( | ||
| CONF_AIRCO_ID, | ||
| CONF_AVAILABILITY_CHECK, | ||
| CONF_AVAILABILITY_RETRY_LIMIT, | ||
| CONF_CONNECTION_METHOD, | ||
| CONF_OPERATOR_ID, | ||
| DOMAIN, | ||
| ) | ||
| from .coordinator import ( | ||
| AVAILABILITY_FAILURE_LIMIT_MIN, | ||
| Device, | ||
| registration_full_issue_id, | ||
| ) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| PLATFORMS = [Platform.CLIMATE] | ||
|
|
||
|
|
||
| @dataclass | ||
| class MitsubishiWfRacData: | ||
| """Class for storing runtime data.""" | ||
|
|
||
| device: Device | ||
|
|
||
|
|
||
| type MitsubishiWfRacConfigEntry = ConfigEntry[MitsubishiWfRacData] | ||
|
|
||
|
|
||
| async def async_migrate_entry( | ||
| hass: HomeAssistant, entry: MitsubishiWfRacConfigEntry | ||
| ) -> bool: | ||
| """Migrate old config entry.""" | ||
|
|
||
| if entry.version == 1: | ||
| new_data = entry.data.copy() | ||
| new_options = { | ||
| CONF_HOST: new_data.pop(CONF_HOST), | ||
| CONF_AVAILABILITY_CHECK: False, | ||
| CONF_AVAILABILITY_RETRY_LIMIT: 3, | ||
| } | ||
|
|
||
| hass.config_entries.async_update_entry( | ||
| entry, data=new_data, options=new_options, version=2 | ||
| ) | ||
| if entry.version == 2: | ||
| # This step used to write an "availability_retry" key that nothing ever | ||
| # reads, and to reset CONF_AVAILABILITY_RETRY_LIMIT back to 3 over any | ||
| # value the user had picked. Both are gone; the version bump is all that | ||
| # is left. Entries that already ran the old step get the stale key | ||
| # cleaned up by the v3 -> v4 step below. | ||
| hass.config_entries.async_update_entry(entry, version=3) | ||
| if entry.version == 3: | ||
| new_options = dict(entry.options) | ||
| new_options.pop("availability_retry", None) | ||
| # The v1 -> v2 step above hard-set CONF_AVAILABILITY_CHECK to False at a | ||
| # time when the flag was dead code (see create_device_from_entry), so | ||
| # every entry predating v2 has been running with no retry tolerance at | ||
| # all: one failed poll marks the device unavailable. The WF-RAC module | ||
| # reassociates on its own roughly once an hour, which a 60s poll | ||
| # interval turns into a visible outage. Turn the check on, and lift | ||
| # limits below 2, which are equivalent to it being off (Device. | ||
| # _set_availability() needs limit-1 consecutive failures to tolerate). | ||
| new_options[CONF_AVAILABILITY_CHECK] = True | ||
| if new_options.get(CONF_AVAILABILITY_RETRY_LIMIT, 3) < 2: | ||
| new_options[CONF_AVAILABILITY_RETRY_LIMIT] = 3 | ||
|
|
||
| hass.config_entries.async_update_entry(entry, options=new_options, version=4) | ||
| if entry.version == 4: | ||
| # Drop the on/off toggle and put a floor under the retry limit. The | ||
| # toggle was never a defensible choice - the module's hourly | ||
| # reassociation makes some tolerance always right, and switching it off | ||
| # was arithmetically identical to a limit of 1. Raising the limit is a | ||
| # real choice on a weak link, so the number stays; only values below | ||
| # AVAILABILITY_FAILURE_LIMIT_MIN are lifted, which is what the v3 -> v4 | ||
| # step above was already having to do by hand. | ||
| new_options = dict(entry.options) | ||
| new_options.pop(CONF_AVAILABILITY_CHECK, None) | ||
| new_options[CONF_AVAILABILITY_RETRY_LIMIT] = max( | ||
| AVAILABILITY_FAILURE_LIMIT_MIN, | ||
| new_options.get( | ||
| CONF_AVAILABILITY_RETRY_LIMIT, AVAILABILITY_FAILURE_LIMIT_MIN | ||
| ), | ||
| ) | ||
|
|
||
| hass.config_entries.async_update_entry(entry, options=new_options, version=5) | ||
| if entry.version == 5: | ||
| # Move the host back into entry.data, where connection-critical data | ||
| # belongs. It lived in options since v2 so it could be edited there, | ||
| # which the reconfigure flow now does instead - and options was the | ||
| # wrong home for a second reason: the discovery helper that refreshes | ||
| # a changed address (_abort_if_unique_id_configured(updates=...)) | ||
| # only ever merges into entry.data, so the refresh wrote a key setup | ||
| # never read and the address silently stayed stale. | ||
| new_data = dict(entry.data) | ||
| new_options = dict(entry.options) | ||
| if CONF_HOST in new_options: | ||
| new_data[CONF_HOST] = new_options.pop(CONF_HOST) | ||
|
|
||
| hass.config_entries.async_update_entry( | ||
| entry, data=new_data, options=new_options, version=6 | ||
| ) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, entry: MitsubishiWfRacConfigEntry | ||
| ) -> bool: | ||
| """Establish connection with mitsubishi-wf-rac.""" | ||
| device: str = entry.data[CONF_HOST] | ||
| _device = await create_device_from_entry(entry, hass) | ||
|
|
||
| await _device.update() # initial update to get fresh values | ||
| # update() catches its own errors and reflects them via .available instead | ||
| # of raising (see coordinator.py) - check that instead of try/except so a | ||
| # device that's unreachable at startup gets HA's automatic retry-with-backoff | ||
| # rather than a silently "loaded" entry with no working entities. | ||
| if not _device.available: | ||
| raise ConfigEntryNotReady( | ||
| translation_domain=DOMAIN, | ||
| translation_key="cannot_connect", | ||
| translation_placeholders={"device": device}, | ||
| ) | ||
|
|
||
| # Persist the discovered connection method (http/https) so we can skip | ||
| # protocol discovery (and its potential extra round-trip) after the next | ||
| # restart. Writing entry.data here is safe on its own: nothing listens for | ||
| # entry updates any more, the options flow reloads itself instead. | ||
| method = _device.connection_method | ||
| if method and entry.data.get(CONF_CONNECTION_METHOD) != method: | ||
| hass.config_entries.async_update_entry( | ||
| entry, data={**entry.data, CONF_CONNECTION_METHOD: method} | ||
| ) | ||
| _LOGGER.debug( | ||
| "Persisted connection method [%s] for device [%s]", method, device | ||
| ) | ||
|
|
||
| entry.runtime_data = MitsubishiWfRacData(_device) | ||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def create_device_from_entry( | ||
| entry: MitsubishiWfRacConfigEntry, hass: HomeAssistant | ||
| ) -> Device: | ||
| """Build the coordinator for a config entry.""" | ||
| device: str = entry.data[CONF_HOST] | ||
| name: str = entry.data[CONF_NAME] | ||
| device_id: str = entry.data[CONF_DEVICE_ID] | ||
| operator_id: str = entry.data[CONF_OPERATOR_ID] | ||
| port: int = entry.data[CONF_PORT] | ||
| airco_id: str = entry.data[CONF_AIRCO_ID] | ||
| # Floored in Device itself, so an entry that predates the v4 -> v5 | ||
| # migration can't run with less tolerance than the module needs. | ||
| availability_failure_limit: int = entry.options.get( | ||
| CONF_AVAILABILITY_RETRY_LIMIT, AVAILABILITY_FAILURE_LIMIT_MIN | ||
| ) | ||
| connection_method: str | None = entry.data.get(CONF_CONNECTION_METHOD) | ||
| return Device( | ||
| hass, | ||
| entry, | ||
| name, | ||
| device, | ||
| port, | ||
| device_id, | ||
| operator_id, | ||
| airco_id, | ||
| availability_failure_limit=availability_failure_limit, | ||
| connection_method=connection_method, | ||
| ) | ||
|
|
||
|
|
||
| async def async_unload_entry( | ||
| hass: HomeAssistant, entry: MitsubishiWfRacConfigEntry | ||
| ) -> bool: | ||
| """Handle unload of entry.""" | ||
|
|
||
| # Unload entities for this entry/device. | ||
| unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
|
|
||
| # Only tear the coordinator down once the entities are really gone: if | ||
| # unloading the platforms failed they stay loaded, and stopping their | ||
| # coordinator would leave a loaded entry that never updates again. | ||
| # An entry whose setup never got as far as storing its runtime data can | ||
| # still be unloaded - there is simply no coordinator to shut down then. | ||
| if unload_ok and (data := getattr(entry, "runtime_data", None)) is not None: | ||
| await data.device.async_shutdown() | ||
|
|
||
| if unload_ok: | ||
|
Comment on lines
+203
to
+208
|
||
| _LOGGER.info("Unloaded entry for device [%s]", entry.data[CONF_NAME]) | ||
| else: | ||
| _LOGGER.warning("Failed to unload entry for device [%s]", entry.data[CONF_NAME]) | ||
|
|
||
| return unload_ok | ||
|
|
||
|
|
||
| async def async_remove_entry( | ||
| hass: HomeAssistant, entry: MitsubishiWfRacConfigEntry | ||
| ) -> None: | ||
| """Handle removal of an entry.""" | ||
|
|
||
| temp_device = await create_device_from_entry(entry, hass) | ||
| # delete_account() catches its own errors and returns None on failure (see | ||
| # coordinator.py) rather than raising, so check the result instead of | ||
| # try/except - the previous try/except here could never actually trigger, | ||
| # and the "Deleted" log below used to fire unconditionally even on failure. | ||
| result = await temp_device.delete_account() | ||
| if result is not None: | ||
| _LOGGER.info( | ||
| "Deleted operator ID [%s] from airco [%s]", | ||
| temp_device.operator_id, | ||
| temp_device.airco_id, | ||
| ) | ||
| else: | ||
| _LOGGER.warning( | ||
| "Could not delete operator ID [%s] from airco [%s]", | ||
| temp_device.operator_id, | ||
| temp_device.airco_id, | ||
| ) | ||
|
|
||
| # Entry-scoped, so it would otherwise dangle in the repair list forever | ||
| # pointing at an entry_id that no longer resolves to anything. | ||
| ir.async_delete_issue(hass, DOMAIN, registration_full_issue_id(entry.entry_id)) | ||
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't there be no versions, since this is a new integration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair question, and it would be right for a genuinely new integration.
The catch is the domain.
mitsubishi_wf_racis not new to the installationsthis lands on: it has existed as a custom component for a while, with roughly
1,900 installations, and it uses this exact domain. When one of them removes
the custom component and picks up the core integration, Home Assistant loads
the config entries that are already there — and those carry versions 1 through
5 from the custom component's own history.
Without the migration each of those entries fails to load with a "config entry
version is newer/older" error, and the user is left deleting and re-adding
every unit by hand. With it, they carry over.
The versions themselves are the custom component's, not invented for this PR.
The last step is the one this branch added: v5 → v6 moves the host from
optionsback intodata, so the discovery helper that refreshes a changedaddress writes it where setup reads it.