Compare commits

..

3 Commits

Author SHA1 Message Date
jbouwh
be5dfcd06d Automatically update the entity propery when a member created, updated or deleted 2025-09-15 17:49:40 +00:00
jbouwh
29bda601cf Apply light group icon to all MQTT light schemas 2025-09-15 17:49:10 +00:00
jbouwh
3f31402d0e Allow an MQTT entity to show as a group 2025-09-15 17:49:10 +00:00
387 changed files with 5377 additions and 15561 deletions

View File

@@ -198,7 +198,7 @@ jobs:
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@2025.09.0
uses: home-assistant/builder@2025.03.0
with:
args: |
$BUILD_ARGS \
@@ -265,7 +265,7 @@ jobs:
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@2025.09.0
uses: home-assistant/builder@2025.03.0
with:
args: |
$BUILD_ARGS \

View File

@@ -523,24 +523,22 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Check if apt cache exists
id: cache-apt-check
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
- name: Restore apt cache
if: steps.cache-venv.outputs.cache-hit != 'true'
id: cache-apt
uses: actions/cache@v4.2.4
with:
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Install additional OS dependencies
if: |
steps.cache-venv.outputs.cache-hit != 'true'
|| steps.cache-apt-check.outputs.cache-hit != 'true'
if: steps.cache-venv.outputs.cache-hit != 'true'
timeout-minutes: 10
run: |
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
if [[ "${{ steps.cache-apt-check.outputs.cache-hit }}" != 'true' ]]; then
if [[ "${{ steps.cache-apt.outputs.cache-hit }}" != 'true' ]]; then
mkdir -p ${{ env.APT_CACHE_DIR }}
mkdir -p ${{ env.APT_LIST_CACHE_DIR }}
fi
@@ -565,18 +563,9 @@ jobs:
libswscale-dev \
libudev-dev
if [[ "${{ steps.cache-apt-check.outputs.cache-hit }}" != 'true' ]]; then
if [[ "${{ steps.cache-apt.outputs.cache-hit }}" != 'true' ]]; then
sudo chmod -R 755 ${{ env.APT_CACHE_BASE }}
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |

View File

@@ -142,7 +142,6 @@ homeassistant.components.cloud.*
homeassistant.components.co2signal.*
homeassistant.components.comelit.*
homeassistant.components.command_line.*
homeassistant.components.compit.*
homeassistant.components.config.*
homeassistant.components.configurator.*
homeassistant.components.cookidoo.*

20
CODEOWNERS generated
View File

@@ -107,8 +107,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/ambient_station/ @bachya
/tests/components/ambient_station/ @bachya
/homeassistant/components/amcrest/ @flacjacket
/homeassistant/components/analytics/ @home-assistant/core
/tests/components/analytics/ @home-assistant/core
/homeassistant/components/analytics/ @home-assistant/core @ludeeus
/tests/components/analytics/ @home-assistant/core @ludeeus
/homeassistant/components/analytics_insights/ @joostlek
/tests/components/analytics_insights/ @joostlek
/homeassistant/components/android_ip_webcam/ @engrbm87
@@ -292,8 +292,6 @@ build.json @home-assistant/supervisor
/tests/components/command_line/ @gjohansson-ST
/homeassistant/components/compensation/ @Petro31
/tests/components/compensation/ @Petro31
/homeassistant/components/compit/ @Przemko92
/tests/components/compit/ @Przemko92
/homeassistant/components/config/ @home-assistant/core
/tests/components/config/ @home-assistant/core
/homeassistant/components/configurator/ @home-assistant/core
@@ -1352,8 +1350,6 @@ build.json @home-assistant/supervisor
/tests/components/samsungtv/ @chemelli74 @epenet
/homeassistant/components/sanix/ @tomaszsluszniak
/tests/components/sanix/ @tomaszsluszniak
/homeassistant/components/satel_integra/ @Tommatheussen
/tests/components/satel_integra/ @Tommatheussen
/homeassistant/components/scene/ @home-assistant/core
/tests/components/scene/ @home-assistant/core
/homeassistant/components/schedule/ @home-assistant/core
@@ -1535,8 +1531,8 @@ build.json @home-assistant/supervisor
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur @XiaoLing-git
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur @XiaoLing-git
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
/tests/components/switcher_kis/ @thecode @YogevBokobza
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
@@ -1681,8 +1677,6 @@ build.json @home-assistant/supervisor
/tests/components/uptime_kuma/ @tr4nt0r
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
/tests/components/uptimerobot/ @ludeeus @chemelli74
/homeassistant/components/usage_prediction/ @home-assistant/core
/tests/components/usage_prediction/ @home-assistant/core
/homeassistant/components/usb/ @bdraco
/tests/components/usb/ @bdraco
/homeassistant/components/usgs_earthquakes_feed/ @exxamalte
@@ -1712,8 +1706,6 @@ build.json @home-assistant/supervisor
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
/homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
/tests/components/victron_remote_monitoring/ @AndyTempel
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vivotek/ @HarlemSquirrel
@@ -1729,8 +1721,8 @@ build.json @home-assistant/supervisor
/tests/components/volumio/ @OnFreund
/homeassistant/components/volvo/ @thomasddn
/tests/components/volvo/ @thomasddn
/homeassistant/components/volvooncall/ @molobrakos @svrooij
/tests/components/volvooncall/ @molobrakos @svrooij
/homeassistant/components/volvooncall/ @molobrakos
/tests/components/volvooncall/ @molobrakos
/homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam

View File

@@ -2,31 +2,21 @@
from __future__ import annotations
from pathlib import Path
from homeassistant.components.media_source import MediaSource, local_source
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import DATA_MEDIA_SOURCE, DOMAIN, IMAGE_DIR
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up local media source."""
media_dirs = list(hass.config.media_dirs.values())
if not media_dirs:
raise HomeAssistantError(
"AI Task media source requires at least one media directory configured"
)
media_dir = Path(media_dirs[0]) / DOMAIN / IMAGE_DIR
media_dir = hass.config.path(f"{DOMAIN}/{IMAGE_DIR}")
hass.data[DATA_MEDIA_SOURCE] = source = local_source.LocalSource(
hass,
DOMAIN,
"AI Generated Images",
{IMAGE_DIR: str(media_dir)},
{IMAGE_DIR: media_dir},
f"/{DOMAIN}",
)
return source

View File

@@ -12,7 +12,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.components import camera, conversation, image, media_source
from homeassistant.components import camera, conversation, media_source
from homeassistant.components.http.auth import async_sign_path
from homeassistant.core import HomeAssistant, ServiceResponse, callback
from homeassistant.exceptions import HomeAssistantError
@@ -31,14 +31,14 @@ from .const import (
)
def _save_camera_snapshot(image_data: camera.Image | image.Image) -> Path:
def _save_camera_snapshot(image: camera.Image) -> Path:
"""Save camera snapshot to temp file."""
with tempfile.NamedTemporaryFile(
mode="wb",
suffix=mimetypes.guess_extension(image_data.content_type, False),
suffix=mimetypes.guess_extension(image.content_type, False),
delete=False,
) as temp_file:
temp_file.write(image_data.content)
temp_file.write(image.content)
return Path(temp_file.name)
@@ -54,31 +54,26 @@ async def _resolve_attachments(
for attachment in attachments or []:
media_content_id = attachment["media_content_id"]
# Special case for certain media sources
for integration in camera, image:
media_source_prefix = f"media-source://{integration.DOMAIN}/"
if not media_content_id.startswith(media_source_prefix):
continue
# Special case for camera media sources
if media_content_id.startswith("media-source://camera/"):
# Extract entity_id from the media content ID
entity_id = media_content_id.removeprefix(media_source_prefix)
entity_id = media_content_id.removeprefix("media-source://camera/")
# Get snapshot from entity
image_data = await integration.async_get_image(hass, entity_id)
# Get snapshot from camera
image = await camera.async_get_image(hass, entity_id)
temp_filename = await hass.async_add_executor_job(
_save_camera_snapshot, image_data
_save_camera_snapshot, image
)
created_files.append(temp_filename)
resolved_attachments.append(
conversation.Attachment(
media_content_id=media_content_id,
mime_type=image_data.content_type,
mime_type=image.content_type,
path=temp_filename,
)
)
break
else:
# Handle regular media sources
media = await media_source.async_resolve_media(hass, media_content_id, None)

View File

@@ -2,7 +2,7 @@
"domain": "analytics",
"name": "Analytics",
"after_dependencies": ["energy", "hassio", "recorder"],
"codeowners": ["@home-assistant/core"],
"codeowners": ["@home-assistant/core", "@ludeeus"],
"dependencies": ["api", "websocket_api", "http"],
"documentation": "https://www.home-assistant.io/integrations/analytics",
"integration_type": "system",

View File

@@ -467,10 +467,7 @@ async def async_setup_entry(
# periodical (or manual) self test since last daemon restart. It might not be available
# when we set up the integration, and we do not know if it would ever be available. Here we
# add it anyway and mark it as unknown initially.
#
# We also sort the resources to ensure the order of entities created is deterministic since
# "APCMODEL" and "MODEL" resources map to the same "Model" name.
for resource in sorted(available_resources | {LAST_S_TEST}):
for resource in available_resources | {LAST_S_TEST}:
if resource not in SENSORS:
_LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper())
continue

View File

@@ -109,7 +109,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
)
state = await self.async_get_last_state()
if (state is not None) and (state.state in self.options):
if state is not None and state.state in self.options:
self._attr_current_option = state.state
if self.registry_entry and (device_id := self.registry_entry.device_id):
@@ -119,7 +119,7 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
def cleanup() -> None:
"""Clean up registered device."""
pipeline_data.pipeline_devices.pop(device_id, None)
pipeline_data.pipeline_devices.pop(device_id)
self.async_on_remove(cleanup)

View File

@@ -120,7 +120,6 @@ class AsusWrtBridge(ABC):
def __init__(self, host: str) -> None:
"""Initialize Bridge."""
self._configuration_url = f"http://{host}"
self._host = host
self._firmware: str | None = None
self._label_mac: str | None = None
@@ -128,11 +127,6 @@ class AsusWrtBridge(ABC):
self._model_id: str | None = None
self._serial_number: str | None = None
@property
def configuration_url(self) -> str:
"""Return configuration URL."""
return self._configuration_url
@property
def host(self) -> str:
"""Return hostname."""
@@ -377,7 +371,6 @@ class AsusWrtHttpBridge(AsusWrtBridge):
# get main router properties
if mac := _identity.mac:
self._label_mac = format_mac(mac)
self._configuration_url = self._api.webpanel
self._firmware = str(_identity.firmware)
self._model = _identity.model
self._model_id = _identity.product_id

View File

@@ -388,13 +388,13 @@ class AsusWrtRouter:
def device_info(self) -> DeviceInfo:
"""Return the device information."""
info = DeviceInfo(
configuration_url=self._api.configuration_url,
identifiers={(DOMAIN, self._entry.unique_id or "AsusWRT")},
name=self.host,
model=self._api.model or "Asus Router",
model_id=self._api.model_id,
serial_number=self._api.serial_number,
manufacturer="Asus",
configuration_url=f"http://{self.host}",
)
if self._api.firmware:
info["sw_version"] = self._api.firmware

View File

@@ -2,12 +2,13 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine
import logging
from typing import Any
from aiohttp import ClientResponseError
from yalexs.activity import ActivityType
from yalexs.lock import Lock, LockOperation, LockStatus
from yalexs.activity import ActivityType, ActivityTypes
from yalexs.lock import Lock, LockStatus
from yalexs.util import get_latest_activity, update_lock_detail_from_activity
from homeassistant.components.lock import ATTR_CHANGED_BY, LockEntity, LockEntityFeature
@@ -49,25 +50,30 @@ class AugustLock(AugustEntity, RestoreEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
await self._perform_lock_operation(LockOperation.LOCK)
if self._data.push_updates_connected:
await self._data.async_lock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_lock)
async def async_open(self, **kwargs: Any) -> None:
"""Open/unlatch the device."""
await self._perform_lock_operation(LockOperation.OPEN)
if self._data.push_updates_connected:
await self._data.async_unlatch_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlatch)
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
await self._perform_lock_operation(LockOperation.UNLOCK)
if self._data.push_updates_connected:
await self._data.async_unlock_async(self._device_id, self._hyper_bridge)
return
await self._call_lock_operation(self._data.async_unlock)
async def _perform_lock_operation(self, operation: LockOperation) -> None:
"""Perform a lock operation."""
async def _call_lock_operation(
self, lock_operation: Callable[[str], Coroutine[Any, Any, list[ActivityTypes]]]
) -> None:
try:
activities = await self._data.async_operate_lock(
self._device_id,
operation,
self._data.push_updates_connected,
self._hyper_bridge,
)
activities = await lock_operation(self._device_id)
except ClientResponseError as err:
if err.status == LOCK_JAMMED_ERR:
self._detail.lock_status = LockStatus.JAMMED

View File

@@ -29,5 +29,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.1.2"]
"requirements": ["yalexs==9.0.1", "yalexs-ble==3.1.2"]
}

View File

@@ -26,6 +26,7 @@ EXCLUDE_FROM_BACKUP = [
"tmp_backups/*.tar",
"OZW_Log.txt",
"tts/*",
"ai_task/*",
]
EXCLUDE_DATABASE_FROM_BACKUP = [

View File

@@ -8,7 +8,7 @@ import threading
from typing import IO, cast
from aiohttp import BodyPartReader
from aiohttp.hdrs import CONTENT_DISPOSITION, CONTENT_TYPE
from aiohttp.hdrs import CONTENT_DISPOSITION
from aiohttp.web import FileResponse, Request, Response, StreamResponse
from multidict import istr
@@ -76,8 +76,7 @@ class DownloadBackupView(HomeAssistantView):
return Response(status=HTTPStatus.NOT_FOUND)
headers = {
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar",
CONTENT_TYPE: "application/x-tar",
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
}
try:

View File

@@ -37,10 +37,6 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.loader import (
async_get_custom_components,
async_get_loaded_integration,
)
from homeassistant.util.location import async_detect_location_info
from .alexa_config import entity_supported as entity_supported_by_alexa
@@ -435,79 +431,6 @@ class DownloadSupportPackageView(HomeAssistantView):
url = "/api/cloud/support_package"
name = "api:cloud:support_package"
async def _get_integration_info(self, hass: HomeAssistant) -> dict[str, Any]:
"""Collect information about active and custom integrations."""
# Get loaded components from hass.config.components
loaded_components = hass.config.components.copy()
# Get custom integrations
custom_domains = set()
with suppress(Exception):
custom_domains = set(await async_get_custom_components(hass))
# Separate built-in and custom integrations
builtin_integrations = []
custom_integrations = []
for domain in sorted(loaded_components):
try:
integration = async_get_loaded_integration(hass, domain)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package
# generation. If we can't get integration info,
# just add the domain
if domain in custom_domains:
custom_integrations.append(
{
"domain": domain,
"name": "Unknown",
"version": "Unknown",
"documentation": "Unknown",
}
)
else:
builtin_integrations.append(
{
"domain": domain,
"name": "Unknown",
}
)
else:
if domain in custom_domains:
# This is a custom integration
# include version and documentation link
version = (
str(integration.version) if integration.version else "Unknown"
)
if not (documentation := integration.documentation):
documentation = "Unknown"
custom_integrations.append(
{
"domain": domain,
"name": integration.name,
"version": version,
"documentation": documentation,
}
)
else:
# This is a built-in integration.
# No version needed, as it is always the same as the
# Home Assistant version
builtin_integrations.append(
{
"domain": domain,
"name": integration.name,
}
)
return {
"builtin_count": len(builtin_integrations),
"builtin_integrations": builtin_integrations,
"custom_count": len(custom_integrations),
"custom_integrations": custom_integrations,
}
async def _generate_markdown(
self,
hass: HomeAssistant,
@@ -530,38 +453,6 @@ class DownloadSupportPackageView(HomeAssistantView):
markdown = "## System Information\n\n"
markdown += get_domain_table_markdown(hass_info)
# Add integration information
try:
integration_info = await self._get_integration_info(hass)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
# If there's any error getting integration info, just note it
markdown += "## Active integrations\n\n"
markdown += "Unable to collect integration information\n\n"
else:
markdown += "## Active Integrations\n\n"
markdown += f"Built-in integrations: {integration_info['builtin_count']}\n"
markdown += f"Custom integrations: {integration_info['custom_count']}\n\n"
# Built-in integrations
if integration_info["builtin_integrations"]:
markdown += "<details><summary>Built-in integrations</summary>\n\n"
markdown += "Domain | Name\n"
markdown += "--- | ---\n"
for integration in integration_info["builtin_integrations"]:
markdown += f"{integration['domain']} | {integration['name']}\n"
markdown += "\n</details>\n\n"
# Custom integrations
if integration_info["custom_integrations"]:
markdown += "<details><summary>Custom integrations</summary>\n\n"
markdown += "Domain | Name | Version | Documentation\n"
markdown += "--- | --- | --- | ---\n"
for integration in integration_info["custom_integrations"]:
doc_url = integration.get("documentation") or "N/A"
markdown += f"{integration['domain']} | {integration['name']} | {integration['version']} | {doc_url}\n"
markdown += "\n</details>\n\n"
for domain, domain_info in domains_info.items():
domain_info_md = get_domain_table_markdown(domain_info)
markdown += (

View File

@@ -2,7 +2,7 @@
from abc import abstractmethod
from datetime import timedelta
from typing import Any, TypeVar
from typing import TypeVar
from aiocomelit.api import (
AlarmDataObject,
@@ -13,16 +13,7 @@ from aiocomelit.api import (
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import (
BRIDGE,
CLIMATE,
COVER,
IRRIGATION,
LIGHT,
OTHER,
SCENARIO,
VEDO,
)
from aiocomelit.const import BRIDGE, VEDO
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession
@@ -120,32 +111,6 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
async def _async_update_system_data(self) -> T:
"""Class method for updating data."""
async def _async_remove_stale_devices(
self,
previous_list: dict[int, Any],
current_list: dict[int, Any],
dev_type: str,
) -> None:
"""Remove stale devices."""
device_registry = dr.async_get(self.hass)
for i in previous_list:
if i not in current_list:
_LOGGER.debug(
"Detected change in %s devices: index %s removed",
dev_type,
i,
)
identifier = f"{self.config_entry.entry_id}-{dev_type}-{i}"
device = device_registry.async_get_device(
identifiers={(DOMAIN, identifier)}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
class ComelitSerialBridge(
ComelitBaseCoordinator[dict[str, dict[int, ComelitSerialBridgeObject]]]
@@ -172,15 +137,7 @@ class ComelitSerialBridge(
self,
) -> dict[str, dict[int, ComelitSerialBridgeObject]]:
"""Specific method for updating data."""
data = await self.api.get_all_devices()
if self.data:
for dev_type in (CLIMATE, COVER, LIGHT, IRRIGATION, OTHER, SCENARIO):
await self._async_remove_stale_devices(
self.data[dev_type], data[dev_type], dev_type
)
return data
return await self.api.get_all_devices()
class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
@@ -206,14 +163,4 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
self,
) -> AlarmDataObject:
"""Specific method for updating data."""
data = await self.api.get_all_areas_and_zones()
if self.data:
for obj_type in ("alarm_areas", "alarm_zones"):
await self._async_remove_stale_devices(
self.data[obj_type],
data[obj_type],
"area" if obj_type == "alarm_areas" else "zone",
)
return data
return await self.api.get_all_areas_and_zones()

View File

@@ -72,7 +72,9 @@ rules:
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices: done
stale-devices:
status: todo
comment: missing implementation
# Platinum
async-dependency: done

View File

@@ -1,45 +0,0 @@
"""The Compit integration."""
from compit_inext_api import CannotConnect, CompitApiConnector, InvalidAuth
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
PLATFORMS = [
Platform.CLIMATE,
]
async def async_setup_entry(hass: HomeAssistant, entry: CompitConfigEntry) -> bool:
"""Set up Compit from a config entry."""
session = async_get_clientsession(hass)
connector = CompitApiConnector(session)
try:
connected = await connector.init(
entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD], hass.config.language
)
except CannotConnect as e:
raise ConfigEntryNotReady(f"Error while connecting to Compit: {e}") from e
except InvalidAuth as e:
raise ConfigEntryAuthFailed(
f"Invalid credentials for {entry.data[CONF_EMAIL]}"
) from e
if not connected:
raise ConfigEntryAuthFailed("Authentication API error")
coordinator = CompitDataUpdateCoordinator(hass, entry, connector)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: CompitConfigEntry) -> bool:
"""Unload an entry for the Compit integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,264 +0,0 @@
"""Module contains the CompitClimate class for controlling climate entities."""
import logging
from typing import Any
from compit_inext_api import Param, Parameter
from compit_inext_api.consts import (
CompitFanMode,
CompitHVACMode,
CompitParameter,
CompitPresetMode,
)
from propcache.api import cached_property
from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_OFF,
PRESET_AWAY,
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER_NAME
from .coordinator import CompitConfigEntry, CompitDataUpdateCoordinator
_LOGGER: logging.Logger = logging.getLogger(__name__)
# Device class for climate devices in Compit system
CLIMATE_DEVICE_CLASS = 10
PARALLEL_UPDATES = 0
COMPIT_MODE_MAP = {
CompitHVACMode.COOL: HVACMode.COOL,
CompitHVACMode.HEAT: HVACMode.HEAT,
CompitHVACMode.OFF: HVACMode.OFF,
}
COMPIT_FANSPEED_MAP = {
CompitFanMode.OFF: FAN_OFF,
CompitFanMode.AUTO: FAN_AUTO,
CompitFanMode.LOW: FAN_LOW,
CompitFanMode.MEDIUM: FAN_MEDIUM,
CompitFanMode.HIGH: FAN_HIGH,
CompitFanMode.HOLIDAY: FAN_AUTO,
}
COMPIT_PRESET_MAP = {
CompitPresetMode.AUTO: PRESET_HOME,
CompitPresetMode.HOLIDAY: PRESET_ECO,
CompitPresetMode.MANUAL: PRESET_NONE,
CompitPresetMode.AWAY: PRESET_AWAY,
}
HVAC_MODE_TO_COMPIT_MODE = {v: k for k, v in COMPIT_MODE_MAP.items()}
FAN_MODE_TO_COMPIT_FAN_MODE = {v: k for k, v in COMPIT_FANSPEED_MAP.items()}
PRESET_MODE_TO_COMPIT_PRESET_MODE = {v: k for k, v in COMPIT_PRESET_MAP.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: CompitConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the CompitClimate platform from a config entry."""
coordinator = entry.runtime_data
climate_entities = []
for device_id in coordinator.connector.devices:
device = coordinator.connector.devices[device_id]
if device.definition.device_class == CLIMATE_DEVICE_CLASS:
climate_entities.append(
CompitClimate(
coordinator,
device_id,
{
parameter.parameter_code: parameter
for parameter in device.definition.parameters
},
device.definition.name,
)
)
async_add_devices(climate_entities)
class CompitClimate(CoordinatorEntity[CompitDataUpdateCoordinator], ClimateEntity):
"""Representation of a Compit climate device."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [*COMPIT_MODE_MAP.values()]
_attr_name = None
_attr_has_entity_name = True
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.PRESET_MODE
)
def __init__(
self,
coordinator: CompitDataUpdateCoordinator,
device_id: int,
parameters: dict[str, Parameter],
device_name: str,
) -> None:
"""Initialize the climate device."""
super().__init__(coordinator)
self._attr_unique_id = f"{device_name}_{device_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(device_id))},
name=device_name,
manufacturer=MANUFACTURER_NAME,
model=device_name,
)
self.parameters = parameters
self.device_id = device_id
self.available_presets: Parameter | None = self.parameters.get(
CompitParameter.PRESET_MODE.value
)
self.available_fan_modes: Parameter | None = self.parameters.get(
CompitParameter.FAN_MODE.value
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available and self.device_id in self.coordinator.connector.devices
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
value = self.get_parameter_value(CompitParameter.CURRENT_TEMPERATURE)
if value is None:
return None
return float(value.value)
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
value = self.get_parameter_value(CompitParameter.SET_TARGET_TEMPERATURE)
if value is None:
return None
return float(value.value)
@cached_property
def preset_modes(self) -> list[str] | None:
"""Return the available preset modes."""
if self.available_presets is None or self.available_presets.details is None:
return []
preset_modes = []
for item in self.available_presets.details:
if item is not None:
ha_preset = COMPIT_PRESET_MAP.get(CompitPresetMode(item.state))
if ha_preset and ha_preset not in preset_modes:
preset_modes.append(ha_preset)
return preset_modes
@cached_property
def fan_modes(self) -> list[str] | None:
"""Return the available fan modes."""
if self.available_fan_modes is None or self.available_fan_modes.details is None:
return []
fan_modes = []
for item in self.available_fan_modes.details:
if item is not None:
ha_fan_mode = COMPIT_FANSPEED_MAP.get(CompitFanMode(item.state))
if ha_fan_mode and ha_fan_mode not in fan_modes:
fan_modes.append(ha_fan_mode)
return fan_modes
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
preset_mode = self.get_parameter_value(CompitParameter.PRESET_MODE)
if preset_mode:
compit_preset_mode = CompitPresetMode(preset_mode.value)
return COMPIT_PRESET_MAP.get(compit_preset_mode)
return None
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_mode = self.get_parameter_value(CompitParameter.FAN_MODE)
if fan_mode:
compit_fan_mode = CompitFanMode(fan_mode.value)
return COMPIT_FANSPEED_MAP.get(compit_fan_mode)
return None
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
hvac_mode = self.get_parameter_value(CompitParameter.HVAC_MODE)
if hvac_mode:
compit_hvac_mode = CompitHVACMode(hvac_mode.value)
return COMPIT_MODE_MAP.get(compit_hvac_mode)
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None:
raise ServiceValidationError("Temperature argument missing")
await self.set_parameter_value(CompitParameter.SET_TARGET_TEMPERATURE, temp)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target HVAC mode."""
if not (mode := HVAC_MODE_TO_COMPIT_MODE.get(hvac_mode)):
raise ServiceValidationError(f"Invalid hvac mode {hvac_mode}")
await self.set_parameter_value(CompitParameter.HVAC_MODE, mode.value)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
compit_preset = PRESET_MODE_TO_COMPIT_PRESET_MODE.get(preset_mode)
if compit_preset is None:
raise ServiceValidationError(f"Invalid preset mode: {preset_mode}")
await self.set_parameter_value(CompitParameter.PRESET_MODE, compit_preset.value)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
compit_fan_mode = FAN_MODE_TO_COMPIT_FAN_MODE.get(fan_mode)
if compit_fan_mode is None:
raise ServiceValidationError(f"Invalid fan mode: {fan_mode}")
await self.set_parameter_value(CompitParameter.FAN_MODE, compit_fan_mode.value)
async def set_parameter_value(self, parameter: CompitParameter, value: int) -> None:
"""Call the API to set a parameter to a new value."""
await self.coordinator.connector.set_device_parameter(
self.device_id, parameter, value
)
self.async_write_ha_state()
def get_parameter_value(self, parameter: CompitParameter) -> Param | None:
"""Get the parameter value from the device state."""
return self.coordinator.connector.get_device_parameter(
self.device_id, parameter
)

View File

@@ -1,110 +0,0 @@
"""Config flow for Compit integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from compit_inext_api import CannotConnect, CompitApiConnector, InvalidAuth
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class CompitConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Compit."""
VERSION = 1
async def async_step_user(
self,
user_input: dict[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_create_clientsession(self.hass)
api = CompitApiConnector(session)
success = False
try:
success = await api.init(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
self.hass.config.language,
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if not success:
# Api returned unexpected result but no exception
_LOGGER.error("Compit api returned unexpected result")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_EMAIL])
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult:
"""Handle re-auth."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm re-authentication."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
reauth_entry_data = reauth_entry.data
if user_input:
# Reuse async_step_user with combined credentials
return await self.async_step_user(
{
CONF_EMAIL: reauth_entry_data[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
description_placeholders={CONF_EMAIL: reauth_entry_data[CONF_EMAIL]},
errors=errors,
)

View File

@@ -1,4 +0,0 @@
"""Constants for the Compit integration."""
DOMAIN = "compit"
MANUFACTURER_NAME = "Compit"

View File

@@ -1,43 +0,0 @@
"""Define an object to manage fetching Compit data."""
from datetime import timedelta
import logging
from compit_inext_api import CompitApiConnector, DeviceInstance
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER: logging.Logger = logging.getLogger(__name__)
type CompitConfigEntry = ConfigEntry[CompitDataUpdateCoordinator]
class CompitDataUpdateCoordinator(DataUpdateCoordinator[dict[int, DeviceInstance]]):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
connector: CompitApiConnector,
) -> None:
"""Initialize."""
self.connector = connector
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
)
async def _async_update_data(self) -> dict[int, DeviceInstance]:
"""Update data via library."""
await self.connector.update_state(device_id=None) # Update all devices
return self.connector.devices

View File

@@ -1,12 +0,0 @@
{
"domain": "compit",
"name": "Compit",
"codeowners": ["@Przemko92"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/compit",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.2.1"]
}

View File

@@ -1,86 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules:
status: exempt
comment: |
This integration does not use any common modules.
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration is a cloud service and does not support discovery.
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: todo
icon-translations:
status: exempt
comment: |
There is no need for icon translations.
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: todo
strict-typing: done

View File

@@ -1,35 +0,0 @@
{
"config": {
"step": {
"user": {
"description": "Please enter your https://inext.compit.pl/ credentials.",
"title": "Connect to Compit iNext",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "The email address of your inext.compit.pl account",
"password": "The password of your inext.compit.pl account"
}
},
"reauth_confirm": {
"description": "Please update your password for {email}",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::compit::config::step::user::data_description::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}

View File

@@ -50,13 +50,14 @@ from .const import (
ATTR_LANGUAGE,
ATTR_TEXT,
DATA_COMPONENT,
DATA_DEFAULT_ENTITY,
DOMAIN,
HOME_ASSISTANT_AGENT,
SERVICE_PROCESS,
SERVICE_RELOAD,
ConversationEntityFeature,
)
from .default_agent import async_setup_default_agent
from .default_agent import DefaultAgent, async_setup_default_agent
from .entity import ConversationEntity
from .http import async_setup as async_setup_conversation_http
from .models import AbstractConversationAgent, ConversationInput, ConversationResult
@@ -141,7 +142,7 @@ def async_unset_agent(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> None:
"""Unset the agent to handle the conversations."""
"""Set the agent to handle the conversations."""
get_agent_manager(hass).async_unset_agent(config_entry.entry_id)
@@ -240,10 +241,10 @@ async def async_handle_sentence_triggers(
Returns None if no match occurred.
"""
agent = get_agent_manager(hass).default_agent
assert agent is not None
default_agent = async_get_agent(hass)
assert isinstance(default_agent, DefaultAgent)
return await agent.async_handle_sentence_triggers(user_input)
return await default_agent.async_handle_sentence_triggers(user_input)
async def async_handle_intents(
@@ -256,10 +257,12 @@ async def async_handle_intents(
Returns None if no match occurred.
"""
agent = get_agent_manager(hass).default_agent
assert agent is not None
default_agent = async_get_agent(hass)
assert isinstance(default_agent, DefaultAgent)
return await agent.async_handle_intents(user_input, intent_filter=intent_filter)
return await default_agent.async_handle_intents(
user_input, intent_filter=intent_filter
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -295,9 +298,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def handle_reload(service: ServiceCall) -> None:
"""Reload intents."""
agent = get_agent_manager(hass).default_agent
if agent is not None:
await agent.async_reload(language=service.data.get(ATTR_LANGUAGE))
await hass.data[DATA_DEFAULT_ENTITY].async_reload(
language=service.data.get(ATTR_LANGUAGE)
)
hass.services.async_register(
DOMAIN,

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import dataclasses
import logging
from typing import TYPE_CHECKING, Any
from typing import Any
import voluptuous as vol
@@ -12,7 +12,7 @@ from homeassistant.core import Context, HomeAssistant, async_get_hass, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, intent, singleton
from .const import DATA_COMPONENT, HOME_ASSISTANT_AGENT
from .const import DATA_COMPONENT, DATA_DEFAULT_ENTITY, HOME_ASSISTANT_AGENT
from .entity import ConversationEntity
from .models import (
AbstractConversationAgent,
@@ -28,9 +28,6 @@ from .trace import (
_LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from .default_agent import DefaultAgent
@singleton.singleton("conversation_agent")
@callback
@@ -52,10 +49,8 @@ def async_get_agent(
hass: HomeAssistant, agent_id: str | None = None
) -> AbstractConversationAgent | ConversationEntity | None:
"""Get specified agent."""
manager = get_agent_manager(hass)
if agent_id is None or agent_id == HOME_ASSISTANT_AGENT:
return manager.default_agent
return hass.data[DATA_DEFAULT_ENTITY]
if "." in agent_id:
return hass.data[DATA_COMPONENT].get_entity(agent_id)
@@ -139,7 +134,6 @@ class AgentManager:
"""Initialize the conversation agents."""
self.hass = hass
self._agents: dict[str, AbstractConversationAgent] = {}
self.default_agent: DefaultAgent | None = None
@callback
def async_get_agent(self, agent_id: str) -> AbstractConversationAgent | None:
@@ -188,7 +182,3 @@ class AgentManager:
def async_unset_agent(self, agent_id: str) -> None:
"""Unset the agent."""
self._agents.pop(agent_id, None)
async def async_setup_default_agent(self, agent: DefaultAgent) -> None:
"""Set up the default agent."""
self.default_agent = agent

View File

@@ -10,9 +10,11 @@ from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from homeassistant.helpers.entity_component import EntityComponent
from .default_agent import DefaultAgent
from .entity import ConversationEntity
DOMAIN = "conversation"
DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
HOME_ASSISTANT_AGENT = "conversation.home_assistant"
ATTR_TEXT = "text"
@@ -24,6 +26,7 @@ SERVICE_PROCESS = "process"
SERVICE_RELOAD = "reload"
DATA_COMPONENT: HassKey[EntityComponent[ConversationEntity]] = HassKey(DOMAIN)
DATA_DEFAULT_ENTITY: HassKey[DefaultAgent] = HassKey(f"{DOMAIN}_default_entity")
class ConversationEntityFeature(IntFlag):

View File

@@ -68,9 +68,13 @@ from homeassistant.helpers.event import async_track_state_added_domain
from homeassistant.util import language as language_util
from homeassistant.util.json import JsonObjectType, json_loads_object
from .agent_manager import get_agent_manager
from .chat_log import AssistantContent, ChatLog
from .const import DOMAIN, ConversationEntityFeature
from .const import (
DATA_DEFAULT_ENTITY,
DEFAULT_EXPOSED_ATTRIBUTES,
DOMAIN,
ConversationEntityFeature,
)
from .entity import ConversationEntity
from .models import ConversationInput, ConversationResult
from .trace import ConversationTraceEventType, async_conversation_trace_append
@@ -79,8 +83,6 @@ _LOGGER = logging.getLogger(__name__)
_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that"
_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"]
_DEFAULT_EXPOSED_ATTRIBUTES = {"device_class"}
REGEX_TYPE = type(re.compile(""))
TRIGGER_CALLBACK_TYPE = Callable[
[ConversationInput, RecognizeResult], Awaitable[str | None]
@@ -207,9 +209,9 @@ async def async_setup_default_agent(
config_intents: dict[str, Any],
) -> None:
"""Set up entity registry listener for the default agent."""
agent = DefaultAgent(hass, config_intents)
await entity_component.async_add_entities([agent])
await get_agent_manager(hass).async_setup_default_agent(agent)
entity = DefaultAgent(hass, config_intents)
await entity_component.async_add_entities([entity])
hass.data[DATA_DEFAULT_ENTITY] = entity
@core.callback
def async_entity_state_listener(
@@ -844,7 +846,7 @@ class DefaultAgent(ConversationEntity):
context = {"domain": state.domain}
if state.attributes:
# Include some attributes
for attr in _DEFAULT_EXPOSED_ATTRIBUTES:
for attr in DEFAULT_EXPOSED_ATTRIBUTES:
if attr not in state.attributes:
continue
context[attr] = state.attributes[attr]

View File

@@ -25,7 +25,7 @@ from .agent_manager import (
async_get_agent,
get_agent_manager,
)
from .const import DATA_COMPONENT
from .const import DATA_COMPONENT, DATA_DEFAULT_ENTITY
from .default_agent import (
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
@@ -169,8 +169,7 @@ async def websocket_list_sentences(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""List custom registered sentences."""
agent = get_agent_manager(hass).default_agent
assert agent is not None
agent = hass.data[DATA_DEFAULT_ENTITY]
sentences = []
for trigger_data in agent.trigger_sentences:
@@ -192,8 +191,7 @@ async def websocket_hass_agent_debug(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Return intents that would be matched by the default agent for a list of sentences."""
agent = get_agent_manager(hass).default_agent
assert agent is not None
agent = hass.data[DATA_DEFAULT_ENTITY]
# Return results for each sentence in the same order as the input.
result_dicts: list[dict[str, Any] | None] = []

View File

@@ -20,8 +20,7 @@ from homeassistant.helpers.script import ScriptRunResult
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import UNDEFINED, ConfigType
from .agent_manager import get_agent_manager
from .const import DOMAIN
from .const import DATA_DEFAULT_ENTITY, DOMAIN
from .models import ConversationInput
@@ -124,6 +123,4 @@ async def async_attach_trigger(
# two trigger copies for who will provide a response.
return None
agent = get_agent_manager(hass).default_agent
assert agent is not None
return agent.register_trigger(sentences, call_action)
return hass.data[DATA_DEFAULT_ENTITY].register_trigger(sentences, call_action)

View File

@@ -19,7 +19,6 @@
"ssdp",
"stream",
"sun",
"usage_prediction",
"usb",
"webhook",
"zeroconf"

View File

@@ -43,5 +43,3 @@ class DelugeSensorType(enum.StrEnum):
UPLOAD_SPEED_SENSOR = "upload_speed"
PROTOCOL_TRAFFIC_UPLOAD_SPEED_SENSOR = "protocol_traffic_upload_speed"
PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR = "protocol_traffic_download_speed"
DOWNLOADING_COUNT_SENSOR = "downloading_count"
SEEDING_COUNT_SENSOR = "seeding_count"

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections import Counter
from datetime import timedelta
from ssl import SSLError
from typing import Any
@@ -15,22 +14,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER, DelugeGetSessionStatusKeys, DelugeSensorType
from .const import LOGGER, DelugeGetSessionStatusKeys
type DelugeConfigEntry = ConfigEntry[DelugeDataUpdateCoordinator]
def count_states(data: dict[str, Any]) -> dict[str, int]:
"""Count the states of the provided torrents."""
counts = Counter(torrent[b"state"].decode() for torrent in data.values())
return {
DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value: counts.get("Downloading", 0),
DelugeSensorType.SEEDING_COUNT_SENSOR.value: counts.get("Seeding", 0),
}
class DelugeDataUpdateCoordinator(
DataUpdateCoordinator[dict[Platform, dict[str, Any]]]
):
@@ -51,22 +39,19 @@ class DelugeDataUpdateCoordinator(
)
self.api = api
def _get_deluge_data(self):
"""Get the latest data from Deluge."""
async def _async_update_data(self) -> dict[Platform, dict[str, Any]]:
"""Get the latest data from Deluge and updates the state."""
data = {}
try:
data["session_status"] = self.api.call(
_data = await self.hass.async_add_executor_job(
self.api.call,
"core.get_session_status",
[iter_member.value for iter_member in list(DelugeGetSessionStatusKeys)],
)
data["torrents_status_state"] = self.api.call(
"core.get_torrents_status", {}, ["state"]
data[Platform.SENSOR] = {k.decode(): v for k, v in _data.items()}
data[Platform.SWITCH] = await self.hass.async_add_executor_job(
self.api.call, "core.get_torrents_status", {}, ["paused"]
)
data["torrents_status_paused"] = self.api.call(
"core.get_torrents_status", {}, ["paused"]
)
except (
ConnectionRefusedError,
TimeoutError,
@@ -81,18 +66,4 @@ class DelugeDataUpdateCoordinator(
) from ex
LOGGER.error("Unknown error connecting to Deluge: %s", ex)
raise
return data
async def _async_update_data(self) -> dict[Platform, dict[str, Any]]:
"""Get the latest data from Deluge and updates the state."""
deluge_data = await self.hass.async_add_executor_job(self._get_deluge_data)
data = {}
data[Platform.SENSOR] = {
k.decode(): v for k, v in deluge_data["session_status"].items()
}
data[Platform.SENSOR].update(count_states(deluge_data["torrents_status_state"]))
data[Platform.SWITCH] = deluge_data["torrents_status_paused"]
return data

View File

@@ -1,12 +0,0 @@
{
"entity": {
"sensor": {
"downloading_count": {
"default": "mdi:download"
},
"seeding_count": {
"default": "mdi:upload"
}
}
}
}

View File

@@ -110,18 +110,6 @@ SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = (
data, DelugeSensorType.PROTOCOL_TRAFFIC_DOWNLOAD_SPEED_SENSOR.value
),
),
DelugeSensorEntityDescription(
key=DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value,
translation_key=DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value,
state_class=SensorStateClass.TOTAL,
value=lambda data: data[DelugeSensorType.DOWNLOADING_COUNT_SENSOR.value],
),
DelugeSensorEntityDescription(
key=DelugeSensorType.SEEDING_COUNT_SENSOR.value,
translation_key=DelugeSensorType.SEEDING_COUNT_SENSOR.value,
state_class=SensorStateClass.TOTAL,
value=lambda data: data[DelugeSensorType.SEEDING_COUNT_SENSOR.value],
),
)

View File

@@ -36,10 +36,6 @@
"idle": "[%key:common::state::idle%]"
}
},
"downloading_count": {
"name": "Downloading count",
"unit_of_measurement": "torrents"
},
"download_speed": {
"name": "Download speed"
},
@@ -49,10 +45,6 @@
"protocol_traffic_upload_speed": {
"name": "Protocol traffic upload speed"
},
"seeding_count": {
"name": "Seeding count",
"unit_of_measurement": "[%key:component::deluge::entity::sensor::downloading_count::unit_of_measurement%]"
},
"upload_speed": {
"name": "Upload speed"
}

View File

@@ -1,23 +0,0 @@
"""Diagnostics support for derivative."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
registry = er.async_get(hass)
entities = registry.entities.get_entries_for_config_entry_id(config_entry.entry_id)
return {
"config_entry": config_entry.as_dict(),
"entity": [entity.extended_dict for entity in entities],
}

View File

@@ -227,28 +227,15 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
weight = calculate_weight(start, end, current_time)
derivative = derivative + (value * Decimal(weight))
_LOGGER.debug(
"%s: Calculated new derivative as %f from %d segments",
self.entity_id,
derivative,
len(self._state_list),
)
return derivative
def _prune_state_list(self, current_time: datetime) -> None:
# filter out all derivatives older than `time_window` from our window list
old_len = len(self._state_list)
self._state_list = [
(time_start, time_end, state)
for time_start, time_end, state in self._state_list
if (current_time - time_end).total_seconds() < self._time_window
]
_LOGGER.debug(
"%s: Pruned %d elements from state list",
self.entity_id,
old_len - len(self._state_list),
)
def _handle_invalid_source_state(self, state: State | None) -> bool:
# Check the source state for unknown/unavailable condition. If unusable, write unknown/unavailable state and return false.
@@ -305,10 +292,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
) -> None:
"""Calculate derivative based on time and reschedule."""
_LOGGER.debug(
"%s: Recalculating derivative due to max_sub_interval time elapsed",
self.entity_id,
)
self._prune_state_list(now)
derivative = self._calc_derivative_from_state_list(now)
self._write_native_value(derivative)
@@ -317,11 +300,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if derivative != 0:
schedule_max_sub_interval_exceeded(source_state)
_LOGGER.debug(
"%s: Scheduling max_sub_interval_callback in %s",
self.entity_id,
self._max_sub_interval,
)
self._cancel_max_sub_interval_exceeded_callback = async_call_later(
self.hass,
self._max_sub_interval,
@@ -331,9 +309,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
@callback
def on_state_reported(event: Event[EventStateReportedData]) -> None:
"""Handle constant sensor state."""
_LOGGER.debug(
"%s: New state reported event: %s", self.entity_id, event.data
)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
@@ -355,7 +330,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
@callback
def on_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle changed sensor state."""
_LOGGER.debug("%s: New state changed event: %s", self.entity_id, event.data)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
@@ -408,32 +382,15 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
/ Decimal(self._unit_prefix)
* Decimal(self._unit_time)
)
_LOGGER.debug(
"%s: Calculated new derivative segment as %f / %f / %f * %f = %f",
self.entity_id,
delta_value,
elapsed_time,
self._unit_prefix,
self._unit_time,
new_derivative,
)
except ValueError as err:
_LOGGER.warning(
"%s: While calculating derivative: %s", self.entity_id, err
)
_LOGGER.warning("While calculating derivative: %s", err)
except DecimalException as err:
_LOGGER.warning(
"%s: Invalid state (%s > %s): %s",
self.entity_id,
old_value,
new_state.state,
err,
"Invalid state (%s > %s): %s", old_value, new_state.state, err
)
except AssertionError as err:
_LOGGER.error(
"%s: Could not calculate derivative: %s", self.entity_id, err
)
_LOGGER.error("Could not calculate derivative: %s", err)
# For total inreasing sensors, the value is expected to continuously increase.
# A negative derivative for a total increasing sensor likely indicates the
@@ -443,10 +400,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
== SensorStateClass.TOTAL_INCREASING
and new_derivative < 0
):
_LOGGER.debug(
"%s: Dropping sample as source total_increasing sensor decreased",
self.entity_id,
)
return
# add latest derivative to the window list

View File

@@ -234,17 +234,6 @@ ECOWITT_SENSORS_MAPPING: Final = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.DISTANCE_MM: SensorEntityDescription(
key="DISTANCE_MM",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.MILLIMETERS,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.HEAT_COUNT: SensorEntityDescription(
key="HEAT_COUNT",
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
),
EcoWittSensorTypes.PM1: SensorEntityDescription(
key="PM1",
device_class=SensorDeviceClass.PM1,
@@ -253,7 +242,6 @@ ECOWITT_SENSORS_MAPPING: Final = {
),
EcoWittSensorTypes.PM4: SensorEntityDescription(
key="PM4",
device_class=SensorDeviceClass.PM4,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/emoncms",
"iot_class": "local_polling",
"requirements": ["pyemoncms==0.1.3"]
"requirements": ["pyemoncms==0.1.2"]
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/emoncms_history",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["pyemoncms==0.1.3"]
"requirements": ["pyemoncms==0.1.2"]
}

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==41.4.0",
"aioesphomeapi==41.0.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.3.0"
],

View File

@@ -1,39 +0,0 @@
"""Sensor entities for Geocaching."""
from typing import cast
from geocachingapi.models import GeocachingCache
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GeocachingDataUpdateCoordinator
# Base class for all platforms
class GeocachingBaseEntity(CoordinatorEntity[GeocachingDataUpdateCoordinator]):
"""Base class for Geocaching sensors."""
_attr_has_entity_name = True
# Base class for cache entities
class GeocachingCacheEntity(GeocachingBaseEntity):
"""Base class for Geocaching cache entities."""
def __init__(
self, coordinator: GeocachingDataUpdateCoordinator, cache: GeocachingCache
) -> None:
"""Initialize the Geocaching cache entity."""
super().__init__(coordinator)
self.cache = cache
# A device can have multiple entities, and for a cache which requires multiple entities we want to group them together.
# Therefore, we create a device for each cache, which holds all related entities.
self._attr_device_info = DeviceInfo(
name=f"Geocache {cache.name}",
identifiers={(DOMAIN, cast(str, cache.reference_code))},
entry_type=DeviceEntryType.SERVICE,
manufacturer=cache.owner.username,
)

View File

@@ -15,24 +15,6 @@
},
"awarded_favorite_points": {
"default": "mdi:heart"
},
"cache_name": {
"default": "mdi:label"
},
"cache_owner": {
"default": "mdi:account"
},
"cache_found_date": {
"default": "mdi:calendar-search"
},
"cache_found": {
"default": "mdi:package-variant-closed-check"
},
"cache_favorite_points": {
"default": "mdi:star-check"
},
"cache_hidden_date": {
"default": "mdi:calendar-badge"
}
}
}

View File

@@ -4,25 +4,18 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import datetime
from typing import cast
from geocachingapi.models import GeocachingCache, GeocachingStatus
from geocachingapi.models import GeocachingStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator
from .entity import GeocachingBaseEntity, GeocachingCacheEntity
@dataclass(frozen=True, kw_only=True)
@@ -32,63 +25,43 @@ class GeocachingSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[GeocachingStatus], str | int | None]
PROFILE_SENSORS: tuple[GeocachingSensorEntityDescription, ...] = (
SENSORS: tuple[GeocachingSensorEntityDescription, ...] = (
GeocachingSensorEntityDescription(
key="find_count",
translation_key="find_count",
native_unit_of_measurement="caches",
value_fn=lambda status: status.user.find_count,
),
GeocachingSensorEntityDescription(
key="hide_count",
translation_key="hide_count",
native_unit_of_measurement="caches",
entity_registry_visible_default=False,
value_fn=lambda status: status.user.hide_count,
),
GeocachingSensorEntityDescription(
key="favorite_points",
translation_key="favorite_points",
native_unit_of_measurement="points",
entity_registry_visible_default=False,
value_fn=lambda status: status.user.favorite_points,
),
GeocachingSensorEntityDescription(
key="souvenir_count",
translation_key="souvenir_count",
native_unit_of_measurement="souvenirs",
value_fn=lambda status: status.user.souvenir_count,
),
GeocachingSensorEntityDescription(
key="awarded_favorite_points",
translation_key="awarded_favorite_points",
native_unit_of_measurement="points",
entity_registry_visible_default=False,
value_fn=lambda status: status.user.awarded_favorite_points,
),
)
@dataclass(frozen=True, kw_only=True)
class GeocachingCacheSensorDescription(SensorEntityDescription):
"""Define Sensor entity description class."""
value_fn: Callable[[GeocachingCache], StateType | datetime.date]
CACHE_SENSORS: tuple[GeocachingCacheSensorDescription, ...] = (
GeocachingCacheSensorDescription(
key="found_date",
device_class=SensorDeviceClass.DATE,
value_fn=lambda cache: cache.found_date_time,
),
GeocachingCacheSensorDescription(
key="favorite_points",
value_fn=lambda cache: cache.favorite_points,
),
GeocachingCacheSensorDescription(
key="hidden_date",
device_class=SensorDeviceClass.DATE,
value_fn=lambda cache: cache.hidden_date,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: GeocachingConfigEntry,
@@ -96,68 +69,14 @@ async def async_setup_entry(
) -> None:
"""Set up a Geocaching sensor entry."""
coordinator = entry.runtime_data
entities: list[Entity] = []
entities.extend(
GeocachingProfileSensor(coordinator, description)
for description in PROFILE_SENSORS
async_add_entities(
GeocachingSensor(coordinator, description) for description in SENSORS
)
status = coordinator.data
# Add entities for tracked caches
entities.extend(
GeoEntityCacheSensorEntity(coordinator, cache, description)
for cache in status.tracked_caches
for description in CACHE_SENSORS
)
async_add_entities(entities)
# Base class for a cache entity.
# Sets the device, ID and translation settings to correctly group the entity to the correct cache device and give it the correct name.
class GeoEntityBaseCache(GeocachingCacheEntity, SensorEntity):
"""Base class for cache entities."""
def __init__(
self,
coordinator: GeocachingDataUpdateCoordinator,
cache: GeocachingCache,
key: str,
) -> None:
"""Initialize the Geocaching sensor."""
super().__init__(coordinator, cache)
self._attr_unique_id = f"{cache.reference_code}_{key}"
# The translation key determines the name of the entity as this is the lookup for the `strings.json` file.
self._attr_translation_key = f"cache_{key}"
class GeoEntityCacheSensorEntity(GeoEntityBaseCache, SensorEntity):
"""Representation of a cache sensor."""
entity_description: GeocachingCacheSensorDescription
def __init__(
self,
coordinator: GeocachingDataUpdateCoordinator,
cache: GeocachingCache,
description: GeocachingCacheSensorDescription,
) -> None:
"""Initialize the Geocaching sensor."""
super().__init__(coordinator, cache, description.key)
self.entity_description = description
@property
def native_value(self) -> StateType | datetime.date:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.cache)
class GeocachingProfileSensor(GeocachingBaseEntity, SensorEntity):
class GeocachingSensor(
CoordinatorEntity[GeocachingDataUpdateCoordinator], SensorEntity
):
"""Representation of a Sensor."""
entity_description: GeocachingSensorEntityDescription

View File

@@ -33,36 +33,11 @@
},
"entity": {
"sensor": {
"find_count": {
"name": "Total finds",
"unit_of_measurement": "caches"
},
"hide_count": {
"name": "Total hides",
"unit_of_measurement": "caches"
},
"favorite_points": {
"name": "Favorite points",
"unit_of_measurement": "points"
},
"souvenir_count": {
"name": "Total souvenirs",
"unit_of_measurement": "souvenirs"
},
"awarded_favorite_points": {
"name": "Awarded favorite points",
"unit_of_measurement": "points"
},
"cache_found_date": {
"name": "Found date"
},
"cache_favorite_points": {
"name": "Favorite points",
"unit_of_measurement": "points"
},
"cache_hidden_date": {
"name": "Hidden date"
}
"find_count": { "name": "Total finds" },
"hide_count": { "name": "Total hides" },
"favorite_points": { "name": "Favorite points" },
"souvenir_count": { "name": "Total souvenirs" },
"awarded_favorite_points": { "name": "Awarded favorite points" }
}
}
}

View File

@@ -29,7 +29,6 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.typing import ConfigType
@@ -71,21 +70,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def generate_content(call: ServiceCall) -> ServiceResponse:
"""Generate content from text and optionally images."""
LOGGER.warning(
"Action '%s.%s' is deprecated and will be removed in the 2026.4.0 release. "
"Please use the 'ai_task.generate_data' action instead",
DOMAIN,
SERVICE_GENERATE_CONTENT,
)
ir.async_create_issue(
hass,
DOMAIN,
"deprecated_generate_content",
breaks_in_ha_version="2026.4.0",
is_fixable=False,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_generate_content",
)
prompt_parts = [call.data[CONF_PROMPT]]

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["google-genai==1.38.0"]
"requirements": ["google-genai==1.29.0"]
}

View File

@@ -150,16 +150,10 @@
}
}
},
"issues": {
"deprecated_generate_content": {
"title": "Deprecated 'generate_content' action",
"description": "Action 'google_generative_ai_conversation.generate_content' is deprecated and will be removed in the 2026.4.0 release. Please use the 'ai_task.generate_data' action instead"
}
},
"services": {
"generate_content": {
"name": "Generate content (deprecated)",
"description": "Generate content from a prompt consisting of text and optionally images (deprecated)",
"name": "Generate content",
"description": "Generate content from a prompt consisting of text and optionally images",
"fields": {
"prompt": {
"name": "Prompt",

View File

@@ -112,14 +112,11 @@ PLACEHOLDER_KEY_ADDON = "addon"
PLACEHOLDER_KEY_ADDON_URL = "addon_url"
PLACEHOLDER_KEY_REFERENCE = "reference"
PLACEHOLDER_KEY_COMPONENTS = "components"
PLACEHOLDER_KEY_FREE_SPACE = "free_space"
ISSUE_KEY_ADDON_BOOT_FAIL = "issue_addon_boot_fail"
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
CORE_CONTAINER = "homeassistant"
SUPERVISOR_CONTAINER = "hassio_supervisor"
@@ -140,24 +137,6 @@ KEY_TO_UPDATE_TYPES: dict[str, set[str]] = {
REQUEST_REFRESH_DELAY = 10
HELP_URLS = {
"help_url": "https://www.home-assistant.io/help/",
"community_url": "https://community.home-assistant.io/",
}
EXTRA_PLACEHOLDERS = {
"issue_mount_mount_failed": {
"storage_url": "/config/storage",
},
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED: HELP_URLS,
ISSUE_KEY_SYSTEM_FREE_SPACE: {
"more_info_free_space": "https://www.home-assistant.io/more-info/free-space",
},
ISSUE_KEY_ADDON_PWNED: {
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
},
}
class SupervisorEntityModel(StrEnum):
"""Supervisor entity model."""

View File

@@ -41,21 +41,17 @@ from .const import (
EVENT_SUPERVISOR_EVENT,
EVENT_SUPERVISOR_UPDATE,
EVENT_SUPPORTED_CHANGED,
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
ISSUE_KEY_SYSTEM_FREE_SPACE,
PLACEHOLDER_KEY_ADDON,
PLACEHOLDER_KEY_ADDON_URL,
PLACEHOLDER_KEY_FREE_SPACE,
PLACEHOLDER_KEY_REFERENCE,
REQUEST_REFRESH_DELAY,
UPDATE_KEY_SUPERVISOR,
)
from .coordinator import get_addons_info, get_host_info
from .coordinator import get_addons_info
from .handler import HassIO, get_supervisor_client
ISSUE_KEY_UNHEALTHY = "unhealthy"
@@ -82,8 +78,6 @@ ISSUE_KEYS_FOR_REPAIRS = {
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
"issue_system_disk_lifetime",
ISSUE_KEY_SYSTEM_FREE_SPACE,
ISSUE_KEY_ADDON_PWNED,
}
_LOGGER = logging.getLogger(__name__)
@@ -247,17 +241,11 @@ class SupervisorIssues:
def add_issue(self, issue: Issue) -> None:
"""Add or update an issue in the list. Create or update a repair if necessary."""
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
placeholders: dict[str, str] = {}
if not issue.suggestions and issue.key in EXTRA_PLACEHOLDERS:
placeholders |= EXTRA_PLACEHOLDERS[issue.key]
placeholders: dict[str, str] | None = None
if issue.reference:
placeholders[PLACEHOLDER_KEY_REFERENCE] = issue.reference
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
if issue.key in {
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
ISSUE_KEY_ADDON_PWNED,
}:
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
f"/hassio/addon/{issue.reference}"
)
@@ -269,19 +257,6 @@ class SupervisorIssues:
else:
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
elif issue.key == ISSUE_KEY_SYSTEM_FREE_SPACE:
host_info = get_host_info(self._hass)
if (
host_info
and "data" in host_info
and "disk_free" in host_info["data"]
):
placeholders[PLACEHOLDER_KEY_FREE_SPACE] = str(
host_info["data"]["disk_free"]
)
else:
placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2"
async_create_issue(
self._hass,
DOMAIN,
@@ -289,7 +264,7 @@ class SupervisorIssues:
is_fixable=bool(issue.suggestions),
severity=IssueSeverity.WARNING,
translation_key=issue.key,
translation_placeholders=placeholders or None,
translation_placeholders=placeholders,
)
self._issues[issue.uuid] = issue

View File

@@ -16,10 +16,8 @@ from homeassistant.data_entry_flow import FlowResult
from . import get_addons_info, get_issues_info
from .const import (
EXTRA_PLACEHOLDERS,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_PWNED,
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
PLACEHOLDER_KEY_ADDON,
PLACEHOLDER_KEY_COMPONENTS,
@@ -28,6 +26,11 @@ from .const import (
from .handler import get_supervisor_client
from .issues import Issue, Suggestion
HELP_URLS = {
"help_url": "https://www.home-assistant.io/help/",
"community_url": "https://community.home-assistant.io/",
}
SUGGESTION_CONFIRMATION_REQUIRED = {
"addon_execute_remove",
"system_adopt_data_disk",
@@ -35,6 +38,14 @@ SUGGESTION_CONFIRMATION_REQUIRED = {
}
EXTRA_PLACEHOLDERS = {
"issue_mount_mount_failed": {
"storage_url": "/config/storage",
},
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED: HELP_URLS,
}
class SupervisorIssueRepairFlow(RepairsFlow):
"""Handler for an issue fixing flow."""
@@ -208,7 +219,6 @@ async def async_create_fix_flow(
if issue and issue.key in {
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
ISSUE_KEY_ADDON_BOOT_FAIL,
ISSUE_KEY_ADDON_PWNED,
}:
return AddonIssueRepairFlow(hass, issue_id)

View File

@@ -52,10 +52,6 @@
}
}
},
"issue_addon_pwned": {
"title": "Insecure secrets detected in add-on configuration",
"description": "Add-on {addon} uses secrets/passwords in its configuration which are detected as not secure. See [pwned passwords and secrets]({more_info_pwned}) for more information on this issue."
},
"issue_mount_mount_failed": {
"title": "Network storage device failed",
"fix_flow": {
@@ -123,10 +119,6 @@
"title": "Disk lifetime exceeding 90%",
"description": "The data disk has exceeded 90% of its expected lifespan. The disk may soon malfunction which can lead to data loss. You should replace it soon and migrate your data."
},
"issue_system_free_space": {
"title": "Data disk is running low on free space",
"description": "The data disk has only {free_space}GB free space left. This may cause issues with system stability and interfere with functionality such as backups and updates. See [clear up storage]({more_info_free_space}) for tips on how to free up space."
},
"unhealthy": {
"title": "Unhealthy system - {reason}",
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
@@ -193,7 +185,7 @@
},
"unsupported_docker_version": {
"title": "Unsupported system - Docker version",
"description": "System is unsupported because the Docker version is out of date. For information about the required version and how to fix this, select Learn more."
"description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this."
},
"unsupported_job_conditions": {
"title": "Unsupported system - Protections disabled",
@@ -209,7 +201,7 @@
},
"unsupported_os": {
"title": "Unsupported system - Operating System",
"description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. For information about supported operating systems and how to fix this, select Learn more."
"description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this."
},
"unsupported_os_agent": {
"title": "Unsupported system - OS-Agent issues",

View File

@@ -1,23 +0,0 @@
"""Diagnostics support for history_stats."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
registry = er.async_get(hass)
entities = registry.entities.get_entries_for_config_entry_id(config_entry.entry_id)
return {
"config_entry": config_entry.as_dict(),
"entity": [entity.extended_dict for entity in entities],
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.81", "babel==2.15.0"]
"requirements": ["holidays==0.80", "babel==2.15.0"]
}

View File

@@ -103,7 +103,6 @@ class HomeAssistantConnectZBT2ConfigFlow(
VERSION = 1
MINOR_VERSION = 1
ZIGBEE_BAUDRATE = 460800
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the config flow."""

View File

@@ -52,16 +52,8 @@
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
"menu_options": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]"
},
"menu_option_descriptions": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]"
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]"
}
},
"confirm_zigbee": {
@@ -83,29 +75,6 @@
"confirm_otbr": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]"
},
"zigbee_installation_type": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::description%]",
"menu_options": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_custom%]"
},
"menu_option_descriptions": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_custom%]"
}
},
"zigbee_integration": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::title%]",
"menu_options": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_other%]"
},
"menu_option_descriptions": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_other%]"
}
}
},
"error": {
@@ -142,15 +111,7 @@
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
"menu_options": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]"
},
"menu_option_descriptions": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]"
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]"
}
},
"confirm_zigbee": {
@@ -172,29 +133,6 @@
"confirm_otbr": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]"
},
"zigbee_installation_type": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::description%]",
"menu_options": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_custom%]"
},
"menu_option_descriptions": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_custom%]"
}
},
"zigbee_integration": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::title%]",
"menu_options": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_other%]"
},
"menu_option_descriptions": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_other%]"
}
}
},
"abort": {

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from abc import ABC, abstractmethod
import asyncio
from enum import StrEnum
import logging
from typing import Any
@@ -24,7 +23,6 @@ from homeassistant.config_entries import (
ConfigEntryBaseFlow,
ConfigFlow,
ConfigFlowResult,
FlowType,
OptionsFlow,
)
from homeassistant.core import callback
@@ -50,31 +48,13 @@ _LOGGER = logging.getLogger(__name__)
STEP_PICK_FIRMWARE_THREAD = "pick_firmware_thread"
STEP_PICK_FIRMWARE_ZIGBEE = "pick_firmware_zigbee"
STEP_PICK_FIRMWARE_THREAD_MIGRATE = "pick_firmware_thread_migrate"
STEP_PICK_FIRMWARE_ZIGBEE_MIGRATE = "pick_firmware_zigbee_migrate"
class PickedFirmwareType(StrEnum):
"""Firmware types that can be picked."""
THREAD = "thread"
ZIGBEE = "zigbee"
class ZigbeeIntegration(StrEnum):
"""Zigbee integrations that can be picked."""
OTHER = "other"
ZHA = "zha"
class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
"""Base flow to install firmware."""
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
_failed_addon_name: str
_failed_addon_reason: str
_picked_firmware_type: PickedFirmwareType
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate base flow."""
@@ -83,7 +63,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
self._probed_firmware_info: FirmwareInfo | None = None
self._device: str | None = None # To be set in a subclass
self._hardware_name: str = "unknown" # To be set in a subclass
self._zigbee_integration = ZigbeeIntegration.ZHA
self.addon_install_task: asyncio.Task | None = None
self.addon_start_task: asyncio.Task | None = None
@@ -126,23 +105,11 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick Thread or Zigbee firmware."""
# Determine if ZHA or Thread are already configured to present migrate options
zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
otbr_entries = self.hass.config_entries.async_entries(OTBR_DOMAIN)
return self.async_show_menu(
step_id="pick_firmware",
menu_options=[
(
STEP_PICK_FIRMWARE_ZIGBEE_MIGRATE
if zha_entries
else STEP_PICK_FIRMWARE_ZIGBEE
),
(
STEP_PICK_FIRMWARE_THREAD_MIGRATE
if otbr_entries
else STEP_PICK_FIRMWARE_THREAD
),
STEP_PICK_FIRMWARE_ZIGBEE,
STEP_PICK_FIRMWARE_THREAD,
],
description_placeholders=self._get_translation_placeholders(),
)
@@ -288,45 +255,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
return self.async_show_progress_done(next_step_id=next_step_id)
async def _configure_and_start_otbr_addon(self) -> None:
"""Configure and start the OTBR addon."""
# Before we start the addon, confirm that the correct firmware is running
# and populate `self._probed_firmware_info` with the correct information
if not await self._probe_firmware_info(probe_methods=(ApplicationType.SPINEL,)):
raise AbortFlow(
"unsupported_firmware",
description_placeholders=self._get_translation_placeholders(),
)
otbr_manager = get_otbr_addon_manager(self.hass)
addon_info = await self._async_get_addon_info(otbr_manager)
assert self._device is not None
new_addon_config = {
**addon_info.options,
"device": self._device,
"baudrate": 460800,
"flow_control": True,
"autoflash_firmware": False,
}
_LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config)
try:
await otbr_manager.async_set_addon_options(new_addon_config)
except AddonError as err:
_LOGGER.error(err)
raise AbortFlow(
"addon_set_config_failed",
description_placeholders={
**self._get_translation_placeholders(),
"addon_name": otbr_manager.addon_name,
},
) from err
await otbr_manager.async_start_addon_waiting()
async def async_step_firmware_download_failed(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -353,85 +281,17 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
},
)
async def async_step_zigbee_installation_type(
async def async_step_pick_firmware_zigbee(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the installation type step."""
return self.async_show_menu(
step_id="zigbee_installation_type",
menu_options=[
"zigbee_intent_recommended",
"zigbee_intent_custom",
],
)
async def async_step_zigbee_intent_recommended(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select recommended installation type."""
self._zigbee_integration = ZigbeeIntegration.ZHA
return await self._async_continue_picked_firmware()
async def async_step_zigbee_intent_custom(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select custom installation type."""
return await self.async_step_zigbee_integration()
async def async_step_zigbee_integration(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select Zigbee integration."""
return self.async_show_menu(
step_id="zigbee_integration",
menu_options=[
"zigbee_integration_zha",
"zigbee_integration_other",
],
)
async def async_step_zigbee_integration_zha(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select ZHA integration."""
self._zigbee_integration = ZigbeeIntegration.ZHA
return await self._async_continue_picked_firmware()
async def async_step_zigbee_integration_other(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Select other Zigbee integration."""
self._zigbee_integration = ZigbeeIntegration.OTHER
return await self._async_continue_picked_firmware()
async def _async_continue_picked_firmware(self) -> ConfigFlowResult:
"""Continue to the picked firmware step."""
"""Pick Zigbee firmware."""
if not await self._probe_firmware_info():
return self.async_abort(
reason="unsupported_firmware",
description_placeholders=self._get_translation_placeholders(),
)
if self._picked_firmware_type == PickedFirmwareType.ZIGBEE:
return await self.async_step_install_zigbee_firmware()
if result := await self._ensure_thread_addon_setup():
return result
return await self.async_step_install_thread_firmware()
async def async_step_pick_firmware_zigbee(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick Zigbee firmware."""
self._picked_firmware_type = PickedFirmwareType.ZIGBEE
return await self.async_step_zigbee_installation_type()
async def async_step_pick_firmware_zigbee_migrate(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick Zigbee firmware. Migration is automatic."""
return await self.async_step_pick_firmware_zigbee()
return await self.async_step_install_zigbee_firmware()
async def async_step_install_zigbee_firmware(
self, user_input: dict[str, Any] | None = None
@@ -457,43 +317,42 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
"""Pre-confirm Zigbee setup."""
# This step is necessary to prevent `user_input` from being passed through
return await self.async_step_continue_zigbee()
return await self.async_step_confirm_zigbee()
async def async_step_continue_zigbee(
async def async_step_confirm_zigbee(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Continue Zigbee setup."""
"""Confirm Zigbee setup."""
assert self._device is not None
assert self._hardware_name is not None
if user_input is None:
return self.async_show_form(
step_id="confirm_zigbee",
description_placeholders=self._get_translation_placeholders(),
)
if not await self._probe_firmware_info(probe_methods=(ApplicationType.EZSP,)):
return self.async_abort(
reason="unsupported_firmware",
description_placeholders=self._get_translation_placeholders(),
)
if self._zigbee_integration == ZigbeeIntegration.OTHER:
return self._async_flow_finished()
result = await self.hass.config_entries.flow.async_init(
await self.hass.config_entries.flow.async_init(
ZHA_DOMAIN,
context={"source": "hardware"},
data={
"name": self._hardware_name,
"port": {
"path": self._device,
"baudrate": self.ZIGBEE_BAUDRATE,
"baudrate": 115200,
"flow_control": "hardware",
},
"radio_type": "ezsp",
},
)
return self._continue_zha_flow(result)
@callback
def _continue_zha_flow(self, zha_result: ConfigFlowResult) -> ConfigFlowResult:
"""Continue the ZHA flow."""
raise NotImplementedError
return self._async_flow_finished()
async def _ensure_thread_addon_setup(self) -> ConfigFlowResult | None:
"""Ensure the OTBR addon is set up and not running."""
@@ -512,7 +371,18 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
return await self.async_step_install_otbr_addon()
if addon_info.state == AddonState.RUNNING:
# Stop the addon before continuing to flash firmware
# We only fail setup if we have an instance of OTBR running *and* it's
# pointing to different hardware
if addon_info.options["device"] != self._device:
return self.async_abort(
reason="otbr_addon_already_running",
description_placeholders={
**self._get_translation_placeholders(),
"addon_name": otbr_manager.addon_name,
},
)
# Otherwise, stop the addon before continuing to flash firmware
await otbr_manager.async_stop_addon()
return None
@@ -521,14 +391,16 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick Thread firmware."""
self._picked_firmware_type = PickedFirmwareType.THREAD
return await self._async_continue_picked_firmware()
if not await self._probe_firmware_info():
return self.async_abort(
reason="unsupported_firmware",
description_placeholders=self._get_translation_placeholders(),
)
async def async_step_pick_firmware_thread_migrate(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick Thread firmware. Migration is automatic."""
return await self.async_step_pick_firmware_thread()
if result := await self._ensure_thread_addon_setup():
return result
return await self.async_step_install_thread_firmware()
async def async_step_install_thread_firmware(
self, user_input: dict[str, Any] | None = None
@@ -581,8 +453,43 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
otbr_manager = get_otbr_addon_manager(self.hass)
if not self.addon_start_task:
# Before we start the addon, confirm that the correct firmware is running
# and populate `self._probed_firmware_info` with the correct information
if not await self._probe_firmware_info(
probe_methods=(ApplicationType.SPINEL,)
):
return self.async_abort(
reason="unsupported_firmware",
description_placeholders=self._get_translation_placeholders(),
)
addon_info = await self._async_get_addon_info(otbr_manager)
assert self._device is not None
new_addon_config = {
**addon_info.options,
"device": self._device,
"baudrate": 460800,
"flow_control": True,
"autoflash_firmware": False,
}
_LOGGER.debug("Reconfiguring OTBR addon with %s", new_addon_config)
try:
await otbr_manager.async_set_addon_options(new_addon_config)
except AddonError as err:
_LOGGER.error(err)
raise AbortFlow(
"addon_set_config_failed",
description_placeholders={
**self._get_translation_placeholders(),
"addon_name": otbr_manager.addon_name,
},
) from err
self.addon_start_task = self.hass.async_create_task(
self._configure_and_start_otbr_addon()
otbr_manager.async_start_addon_waiting()
)
if not self.addon_start_task.done():
@@ -601,9 +508,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
except (AddonError, AbortFlow) as err:
_LOGGER.error(err)
self._failed_addon_name = otbr_manager.addon_name
self._failed_addon_reason = (
err.reason if isinstance(err, AbortFlow) else "addon_start_failed"
)
self._failed_addon_reason = "addon_start_failed"
return self.async_show_progress_done(next_step_id="addon_operation_failed")
finally:
self.addon_start_task = None
@@ -667,21 +572,6 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
return await self.async_step_pick_firmware()
@callback
def _continue_zha_flow(self, zha_result: ConfigFlowResult) -> ConfigFlowResult:
"""Continue the ZHA flow."""
next_flow_id = zha_result["flow_id"]
result = self._async_flow_finished()
return (
self.async_create_entry(
title=result["title"] or self._hardware_name,
data=result["data"],
next_flow=(FlowType.CONFIG_FLOW, next_flow_id),
)
| result # update all items with the child result
)
class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
"""Zigbee and Thread options flow handlers."""
@@ -739,10 +629,3 @@ class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
)
return await super().async_step_pick_firmware_thread(user_input)
@callback
def _continue_zha_flow(self, zha_result: ConfigFlowResult) -> ConfigFlowResult:
"""Continue the ZHA flow."""
# The options flow cannot return a next_flow yet, so we just finish here.
# The options flow should be changed to a reconfigure flow.
return self._async_flow_finished()

View File

@@ -3,19 +3,11 @@
"options": {
"step": {
"pick_firmware": {
"title": "Pick your protocol",
"description": "You can use your {model} for a Zigbee or Thread network. Please check what type of devices you want to add to Home Assistant. You can always change this later.",
"title": "Pick your firmware",
"description": "Let's get started with setting up your {model}. Do you want to use it to set up a Zigbee or Thread network?",
"menu_options": {
"pick_firmware_zigbee": "Use as Zigbee adapter",
"pick_firmware_thread": "Use as Thread adapter",
"pick_firmware_zigbee_migrate": "Migrate Zigbee to a new adapter",
"pick_firmware_thread_migrate": "Migrate Thread to a new adapter"
},
"menu_option_descriptions": {
"pick_firmware_zigbee": "Most common protocol.",
"pick_firmware_thread": "Often used for Matter over Thread devices.",
"pick_firmware_zigbee_migrate": "This will move your Zigbee network to the new adapter.",
"pick_firmware_thread_migrate": "This will migrate your Thread Border Router to the new adapter."
"pick_firmware_zigbee": "Zigbee",
"pick_firmware_thread": "Thread"
}
},
"confirm_zigbee": {
@@ -37,29 +29,6 @@
"confirm_otbr": {
"title": "OpenThread Border Router setup complete",
"description": "Your {model} is now an OpenThread Border Router and will show up in the Thread integration."
},
"zigbee_installation_type": {
"title": "Set up Zigbee",
"description": "Choose the installation type for the Zigbee adapter.",
"menu_options": {
"zigbee_intent_recommended": "Recommended installation",
"zigbee_intent_custom": "Custom"
},
"menu_option_descriptions": {
"zigbee_intent_recommended": "Automatically install and configure Zigbee.",
"zigbee_intent_custom": "Manually install and configure Zigbee, for example with Zigbee2MQTT."
}
},
"zigbee_integration": {
"title": "Select Zigbee method",
"menu_options": {
"zigbee_integration_zha": "Zigbee Home Automation",
"zigbee_integration_other": "Other"
},
"menu_option_descriptions": {
"zigbee_integration_zha": "Lets Home Assistant control a Zigbee network.",
"zigbee_integration_other": "For example if you want to use the adapter with Zigbee2MQTT."
}
}
},
"abort": {

View File

@@ -52,16 +52,8 @@
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
"menu_options": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]"
},
"menu_option_descriptions": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]"
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]"
}
},
"confirm_zigbee": {
@@ -83,29 +75,6 @@
"confirm_otbr": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]"
},
"zigbee_installation_type": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::description%]",
"menu_options": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_custom%]"
},
"menu_option_descriptions": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_custom%]"
}
},
"zigbee_integration": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::title%]",
"menu_options": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_other%]"
},
"menu_option_descriptions": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_other%]"
}
}
},
"error": {
@@ -142,15 +111,7 @@
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
"menu_options": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]"
},
"menu_option_descriptions": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]"
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]"
}
},
"confirm_zigbee": {
@@ -172,29 +133,6 @@
"confirm_otbr": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]"
},
"zigbee_installation_type": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::description%]",
"menu_options": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_custom%]"
},
"menu_option_descriptions": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_custom%]"
}
},
"zigbee_integration": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::title%]",
"menu_options": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_other%]"
},
"menu_option_descriptions": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_other%]"
}
}
},
"abort": {

View File

@@ -92,7 +92,7 @@ class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
firmware_name="Zigbee",
expected_installed_firmware_type=ApplicationType.EZSP,
step_id="install_zigbee_firmware",
next_step_id="pre_confirm_zigbee",
next_step_id="confirm_zigbee",
)
async def async_step_install_thread_firmware(

View File

@@ -75,16 +75,8 @@
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
"menu_options": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_thread_migrate%]"
},
"menu_option_descriptions": {
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee%]",
"pick_firmware_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread%]",
"pick_firmware_zigbee_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_zigbee_migrate%]",
"pick_firmware_thread_migrate": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_option_descriptions::pick_firmware_thread_migrate%]"
"pick_firmware_zigbee": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::menu_options::pick_firmware_zigbee%]"
}
},
"confirm_zigbee": {
@@ -106,29 +98,6 @@
"confirm_otbr": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_otbr::description%]"
},
"zigbee_installation_type": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::title%]",
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::description%]",
"menu_options": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_options::zigbee_intent_custom%]"
},
"menu_option_descriptions": {
"zigbee_intent_recommended": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_recommended%]",
"zigbee_intent_custom": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_installation_type::menu_option_descriptions::zigbee_intent_custom%]"
}
},
"zigbee_integration": {
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::title%]",
"menu_options": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_options::zigbee_integration_other%]"
},
"menu_option_descriptions": {
"zigbee_integration_zha": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_zha%]",
"zigbee_integration_other": "[%key:component::homeassistant_hardware::firmware_picker::options::step::zigbee_integration::menu_option_descriptions::zigbee_integration_other%]"
}
}
},
"error": {

View File

@@ -337,14 +337,7 @@ class HKDevice:
# We need to explicitly poll characteristics to get fresh sensor readings
# before processing the entity map and creating devices.
# Use poll_all=True since entities haven't registered their characteristics yet.
try:
await self.async_update(poll_all=True)
except ValueError as exc:
_LOGGER.debug(
"Accessory %s responded with unparsable response, first update was skipped: %s",
self.unique_id,
exc,
)
await self.async_update(poll_all=True)
await self.async_process_entity_map()

View File

@@ -36,7 +36,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .const import DOMAIN
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
@@ -67,13 +66,15 @@ def to_percentage(value: float | None) -> float | None:
return value * 100 if value is not None else None
def uptime_to_datetime(value: int) -> datetime:
"""Convert seconds to datetime timestamp."""
return utcnow().replace(microsecond=0) - timedelta(seconds=value)
def time_to_datetime(value: int | None) -> datetime | None:
"""Convert seconds to datetime when value is not None."""
return (
utcnow().replace(microsecond=0) - timedelta(seconds=value)
if value is not None
else None
)
uptime_to_stable_datetime = ignore_variance(uptime_to_datetime, timedelta(minutes=5))
SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
HomeWizardSensorEntityDescription(
key="smr_version",
@@ -646,11 +647,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
lambda data: data.system is not None and data.system.uptime_s is not None
),
value_fn=(
lambda data: (
uptime_to_stable_datetime(data.system.uptime_s)
if data.system is not None and data.system.uptime_s is not None
else None
)
lambda data: time_to_datetime(data.system.uptime_s) if data.system else None
),
),
)

View File

@@ -88,7 +88,7 @@
"message": "Honeywell set temperature failed: invalid temperature {temperature}"
},
"temp_failed_range": {
"message": "Honeywell set temperature failed: temperature out of range. Mode: {mode}, Heat temperature: {heat}, Cool temperature: {cool}"
"message": "Honeywell set temperature failed: temperature out of range. Mode: {mode}, Heat Temperuature: {heat}, Cool Temperature: {cool}"
},
"set_hold_failed": {
"message": "Honeywell could not set permanent hold"

View File

@@ -18,6 +18,6 @@
},
"iot_class": "local_polling",
"loggers": ["aiopvapi"],
"requirements": ["aiopvapi==3.2.1"],
"requirements": ["aiopvapi==3.1.1"],
"zeroconf": ["_powerview._tcp.local.", "_PowerView-G3._tcp.local."]
}

View File

@@ -26,8 +26,8 @@
}
},
"verification_code": {
"title": "Apple Account code",
"description": "Please enter the verification code you just received from Apple",
"title": "iCloud verification code",
"description": "Please enter the verification code you just received from iCloud",
"data": {
"verification_code": "Verification code"
}
@@ -47,11 +47,11 @@
"services": {
"update": {
"name": "Update",
"description": "Asks for a state update of all devices linked to an Apple Account.",
"description": "Asks for a state update of all devices linked to an iCloud account.",
"fields": {
"account": {
"name": "Account",
"description": "Your Apple Account username (email)."
"description": "Your iCloud account username (email) or account name."
}
}
},

View File

@@ -105,20 +105,6 @@ async def _async_get_image(image_entity: ImageEntity, timeout: int) -> Image:
raise HomeAssistantError("Unable to get image")
async def async_get_image(
hass: HomeAssistant,
entity_id: str,
timeout: int = 10,
) -> Image:
"""Fetch an image from an image entity."""
component = hass.data[DATA_COMPONENT]
if (image := component.get_entity(entity_id)) is None:
raise HomeAssistantError(f"Image entity {entity_id} not found")
return await _async_get_image(image, timeout)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the image component."""
component = hass.data[DATA_COMPONENT] = EntityComponent[ImageEntity](

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["imgw_pib==1.5.6"]
"requirements": ["imgw_pib==1.5.4"]
}

View File

@@ -42,7 +42,7 @@
"characteristic_missing": "The device is either already connected to Wi-Fi, or no longer able to connect to Wi-Fi. If you want to connect it to another network, try factory resetting it first.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"provision_successful": "The device has successfully connected to the Wi-Fi network.",
"provision_successful_url": "The device has successfully connected to the Wi-Fi network.\n\nPlease finish the setup by following the [setup instructions]({url}).",
"provision_successful_url": "The device has successfully connected to the Wi-Fi network.\n\nPlease visit {url} to finish setup.",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}

View File

@@ -118,31 +118,27 @@ COVER_KNX_SCHEMA = AllSerializeFirst(
vol.Schema(
{
"section_binary_control": KNXSectionFlat(),
vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False),
vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(),
"section_stop_control": KNXSectionFlat(),
vol.Optional(CONF_GA_STOP): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_STEP): GASelector(state=False, valid_dpt="1"),
vol.Optional(CONF_GA_STOP): GASelector(state=False),
vol.Optional(CONF_GA_STEP): GASelector(state=False),
"section_position_control": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_POSITION_SET): GASelector(
state=False, valid_dpt="5.001"
),
vol.Optional(CONF_GA_POSITION_STATE): GASelector(
write=False, valid_dpt="5.001"
),
vol.Optional(CONF_GA_POSITION_SET): GASelector(state=False),
vol.Optional(CONF_GA_POSITION_STATE): GASelector(write=False),
vol.Optional(CoverConf.INVERT_POSITION): selector.BooleanSelector(),
"section_tilt_control": KNXSectionFlat(collapsible=True),
vol.Optional(CONF_GA_ANGLE): GASelector(valid_dpt="5.001"),
vol.Optional(CONF_GA_ANGLE): GASelector(),
vol.Optional(CoverConf.INVERT_ANGLE): selector.BooleanSelector(),
"section_travel_time": KNXSectionFlat(),
vol.Required(
vol.Optional(
CoverConf.TRAVELLING_TIME_UP, default=25
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=1000, step=0.1, unit_of_measurement="s"
)
),
vol.Required(
vol.Optional(
CoverConf.TRAVELLING_TIME_DOWN, default=25
): selector.NumberSelector(
selector.NumberSelectorConfig(
@@ -314,7 +310,7 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
SWITCH_KNX_SCHEMA = vol.Schema(
{
"section_switch": KNXSectionFlat(),
vol.Required(CONF_GA_SWITCH): GASelector(write_required=True, valid_dpt="1"),
vol.Required(CONF_GA_SWITCH): GASelector(write_required=True),
vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(),
vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(),
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),

View File

@@ -2,9 +2,7 @@
import asyncio
import logging
import uuid
from aiohttp import ClientSession
from packaging import version
from pylamarzocco import (
LaMarzoccoBluetoothClient,
@@ -13,7 +11,6 @@ from pylamarzocco import (
)
from pylamarzocco.const import FirmwareType
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.util import InstallationKey, generate_installation_key
from homeassistant.components.bluetooth import async_discovered_service_info
from homeassistant.const import (
@@ -22,14 +19,13 @@ from homeassistant.const import (
CONF_TOKEN,
CONF_USERNAME,
Platform,
__version__,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import (
LaMarzoccoConfigEntry,
LaMarzoccoConfigUpdateCoordinator,
@@ -64,8 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
installation_key=InstallationKey.from_json(entry.data[CONF_INSTALLATION_KEY]),
client=create_client_session(hass),
client=async_create_clientsession(hass),
)
try:
@@ -171,50 +166,45 @@ async def async_migrate_entry(
hass: HomeAssistant, entry: LaMarzoccoConfigEntry
) -> bool:
"""Migrate config entry."""
if entry.version > 4:
if entry.version > 3:
# guard against downgrade from a future version
return False
if entry.version in (1, 2):
if entry.version == 1:
_LOGGER.error(
"Migration from version 1 or 2 is no longer supported, please remove and re-add the integration"
"Migration from version 1 is no longer supported, please remove and re-add the integration"
)
return False
if entry.version == 3:
installation_key = generate_installation_key(str(uuid.uuid4()).lower())
if entry.version == 2:
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
installation_key=installation_key,
client=create_client_session(hass),
)
try:
await cloud_client.async_register_client()
things = await cloud_client.list_things()
except (AuthFail, RequestNotSuccessful) as exc:
_LOGGER.error("Migration failed with error %s", exc)
return False
v3_data = {
CONF_USERNAME: entry.data[CONF_USERNAME],
CONF_PASSWORD: entry.data[CONF_PASSWORD],
CONF_TOKEN: next(
(
thing.ble_auth_token
for thing in things
if thing.serial_number == entry.unique_id
),
None,
),
}
if CONF_MAC in entry.data:
v3_data[CONF_MAC] = entry.data[CONF_MAC]
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_INSTALLATION_KEY: installation_key.to_json(),
},
version=4,
data=v3_data,
version=3,
)
_LOGGER.debug("Migrated La Marzocco config entry to version 4")
_LOGGER.debug("Migrated La Marzocco config entry to version 2")
return True
def create_client_session(hass: HomeAssistant) -> ClientSession:
"""Create a ClientSession with La Marzocco specific headers."""
return async_create_clientsession(
hass,
headers={
"X-Client": "HOME_ASSISTANT",
"X-Client-Build": __version__,
},
)

View File

@@ -5,13 +5,11 @@ from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
import uuid
from aiohttp import ClientSession
from pylamarzocco import LaMarzoccoCloudClient
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
from pylamarzocco.models import Thing
from pylamarzocco.util import InstallationKey, generate_installation_key
import voluptuous as vol
from homeassistant.components.bluetooth import (
@@ -35,6 +33,7 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
@@ -46,8 +45,7 @@ from homeassistant.helpers.selector import (
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from . import create_client_session
from .const import CONF_INSTALLATION_KEY, CONF_USE_BLUETOOTH, DOMAIN
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import LaMarzoccoConfigEntry
CONF_MACHINE = "machine"
@@ -59,10 +57,9 @@ _LOGGER = logging.getLogger(__name__)
class LmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for La Marzocco."""
VERSION = 4
VERSION = 3
_client: ClientSession
_installation_key: InstallationKey
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -86,18 +83,13 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
**user_input,
}
self._client = create_client_session(self.hass)
self._installation_key = generate_installation_key(
str(uuid.uuid4()).lower()
)
self._client = async_create_clientsession(self.hass)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
client=self._client,
installation_key=self._installation_key,
)
try:
await cloud_client.async_register_client()
things = await cloud_client.list_things()
except AuthFail:
_LOGGER.debug("Server rejected login credentials")
@@ -192,7 +184,6 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
title=selected_device.name,
data={
**self._config,
CONF_INSTALLATION_KEY: self._installation_key.to_json(),
CONF_TOKEN: self._things[serial_number].ble_auth_token,
},
)

View File

@@ -5,4 +5,3 @@ from typing import Final
DOMAIN: Final = "lamarzocco"
CONF_USE_BLUETOOTH: Final = "use_bluetooth"
CONF_INSTALLATION_KEY: Final = "installation_key"

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.1.0"]
"requirements": ["pylamarzocco==2.0.11"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["pypck"],
"quality_scale": "bronze",
"requirements": ["pypck==0.8.12", "lcn-frontend==0.2.7"]
"requirements": ["pypck==0.8.10", "lcn-frontend==0.2.7"]
}

View File

@@ -209,11 +209,5 @@
}
}
}
},
"issues": {
"deprecated_entity": {
"title": "{name} is deprecated",
"description": "The Litter-Robot entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your dashboards, automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue."
}
}
}

View File

@@ -6,24 +6,13 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any, Generic
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4, Robot
from pylitterbot import FeederRobot, LitterRobot, Robot
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from .const import DOMAIN
from .coordinator import LitterRobotConfigEntry
from .entity import LitterRobotEntity, _WhiskerEntityT
@@ -37,15 +26,6 @@ class RobotSwitchEntityDescription(SwitchEntityDescription, Generic[_WhiskerEnti
value_fn: Callable[[_WhiskerEntityT], bool]
NIGHT_LIGHT_MODE_ENTITY_DESCRIPTION = RobotSwitchEntityDescription[
LitterRobot | FeederRobot
](
key="night_light_mode_enabled",
translation_key="night_light_mode",
set_fn=lambda robot, value: robot.set_night_light(value),
value_fn=lambda robot: robot.night_light_mode_enabled,
)
SWITCH_MAP: dict[type[Robot], tuple[RobotSwitchEntityDescription, ...]] = {
FeederRobot: (
RobotSwitchEntityDescription[FeederRobot](
@@ -54,10 +34,14 @@ SWITCH_MAP: dict[type[Robot], tuple[RobotSwitchEntityDescription, ...]] = {
set_fn=lambda robot, value: robot.set_gravity_mode(value),
value_fn=lambda robot: robot.gravity_mode_enabled,
),
NIGHT_LIGHT_MODE_ENTITY_DESCRIPTION,
),
LitterRobot3: (NIGHT_LIGHT_MODE_ENTITY_DESCRIPTION,),
Robot: ( # type: ignore[type-abstract] # only used for isinstance check
RobotSwitchEntityDescription[LitterRobot | FeederRobot](
key="night_light_mode_enabled",
translation_key="night_light_mode",
set_fn=lambda robot, value: robot.set_night_light(value),
value_fn=lambda robot: robot.night_light_mode_enabled,
),
RobotSwitchEntityDescription[LitterRobot | FeederRobot](
key="panel_lock_enabled",
translation_key="panel_lockout",
@@ -75,54 +59,13 @@ async def async_setup_entry(
) -> None:
"""Set up Litter-Robot switches using config entry."""
coordinator = entry.runtime_data
entities = [
async_add_entities(
RobotSwitchEntity(robot=robot, coordinator=coordinator, description=description)
for robot in coordinator.account.robots
for robot_type, entity_descriptions in SWITCH_MAP.items()
if isinstance(robot, robot_type)
for description in entity_descriptions
]
ent_reg = er.async_get(hass)
def add_deprecated_entity(
robot: LitterRobot4,
description: RobotSwitchEntityDescription,
entity_cls: type[RobotSwitchEntity],
) -> None:
"""Add deprecated entities."""
unique_id = f"{robot.serial}-{description.key}"
if entity_id := ent_reg.async_get_entity_id(SWITCH_DOMAIN, DOMAIN, unique_id):
entity_entry = ent_reg.async_get(entity_id)
if entity_entry and entity_entry.disabled:
ent_reg.async_remove(entity_id)
async_delete_issue(
hass,
DOMAIN,
f"deprecated_entity_{unique_id}",
)
elif entity_entry:
entities.append(entity_cls(robot, coordinator, description))
async_create_issue(
hass,
DOMAIN,
f"deprecated_entity_{unique_id}",
breaks_in_ha_version="2026.4.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders={
"name": f"{robot.name} {entity_entry.name or entity_entry.original_name}",
"entity": entity_id,
},
)
for robot in coordinator.account.get_robots(LitterRobot4):
add_deprecated_entity(
robot, NIGHT_LIGHT_MODE_ENTITY_DESCRIPTION, RobotSwitchEntity
)
async_add_entities(entities)
)
class RobotSwitchEntity(LitterRobotEntity[_WhiskerEntityT], SwitchEntity):

View File

@@ -8,7 +8,7 @@ import logging
import ssl
from typing import Any, cast
from pylutron_caseta import BUTTON_STATUS_MULTITAP, BUTTON_STATUS_PRESSED
from pylutron_caseta import BUTTON_STATUS_PRESSED
from pylutron_caseta.smartbridge import Smartbridge
import voluptuous as vol
@@ -25,7 +25,6 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
ACTION_MULTITAP,
ACTION_PRESS,
ACTION_RELEASE,
ATTR_ACTION,
@@ -449,8 +448,6 @@ def _async_subscribe_keypad_events(
if event_type == BUTTON_STATUS_PRESSED:
action = ACTION_PRESS
elif event_type == BUTTON_STATUS_MULTITAP:
action = ACTION_MULTITAP
else:
action = ACTION_RELEASE

View File

@@ -29,7 +29,6 @@ ATTR_DEVICE_NAME = "device_name"
ATTR_AREA_NAME = "area_name"
ATTR_ACTION = "action"
ACTION_MULTITAP = "multi_tap"
ACTION_PRESS = "press"
ACTION_RELEASE = "release"

View File

@@ -21,7 +21,6 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
ACTION_MULTITAP,
ACTION_PRESS,
ACTION_RELEASE,
ATTR_ACTION,
@@ -40,7 +39,7 @@ def _reverse_dict(forward_dict: dict) -> dict:
return {v: k for k, v in forward_dict.items()}
SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_MULTITAP, ACTION_RELEASE]
SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE]
LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{

View File

@@ -9,7 +9,7 @@
},
"iot_class": "local_push",
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.25.0"],
"requirements": ["pylutron-caseta==0.24.0"],
"zeroconf": [
{
"type": "_lutron._tcp.local.",

View File

@@ -15,3 +15,8 @@ ID_TYPE_DEVICE_ID = "deviceid"
ID_TYPE_SERIAL = "serial"
FEATUREMAP_ATTRIBUTE_ID = 65532
# vacuum entity service actions
SERVICE_GET_AREAS = "get_areas" # get SupportedAreas and SupportedMaps
SERVICE_SELECT_AREAS = "select_areas" # call SelectAreas Matter command
SERVICE_CLEAN_AREAS = "clean_areas" # call SelectAreas Matter command and start RVC

View File

@@ -150,5 +150,16 @@
"default": "mdi:ev-station"
}
}
},
"services": {
"clean_areas": {
"service": "mdi:robot-vacuum"
},
"get_areas": {
"service": "mdi:map"
},
"select_areas": {
"service": "mdi:map"
}
}
}

View File

@@ -0,0 +1,24 @@
# Service descriptions for Matter integration
get_areas:
target:
entity:
domain: vacuum
select_areas:
target:
entity:
domain: vacuum
fields:
areas:
required: true
example: [1, 3]
clean_areas:
target:
entity:
domain: vacuum
fields:
areas:
required: true
example: [1, 3]

View File

@@ -548,6 +548,30 @@
"description": "The Matter device to add to the other Matter network."
}
}
},
"get_areas": {
"name": "Get areas",
"description": "Returns a list of available areas and maps for robot vacuum cleaners."
},
"select_areas": {
"name": "Select areas",
"description": "Selects the specified areas for cleaning. The areas must be specified as a list of area IDs.",
"fields": {
"areas": {
"name": "Areas",
"description": "A list of area IDs to select."
}
}
},
"clean_areas": {
"name": "Clean areas",
"description": "Instructs the Matter vacuum cleaner to clean the specified areas.",
"fields": {
"areas": {
"name": "Areas",
"description": "A list of area IDs to clean."
}
}
}
}
}

View File

@@ -3,10 +3,12 @@
from __future__ import annotations
from enum import IntEnum
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from chip.clusters import Objects as clusters
from chip.clusters.Objects import NullValue
from matter_server.client.models import device_types
import voluptuous as vol
from homeassistant.components.vacuum import (
StateVacuumEntity,
@@ -16,14 +18,25 @@ from homeassistant.components.vacuum import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import (
HomeAssistant,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import SERVICE_CLEAN_AREAS, SERVICE_GET_AREAS, SERVICE_SELECT_AREAS
from .entity import MatterEntity
from .helpers import get_matter
from .models import MatterDiscoverySchema
ATTR_CURRENT_AREA = "current_area"
ATTR_CURRENT_AREA_NAME = "current_area_name"
ATTR_SELECTED_AREAS = "selected_areas"
class OperationalState(IntEnum):
"""Operational State of the vacuum cleaner.
@@ -56,6 +69,33 @@ async def async_setup_entry(
"""Set up Matter vacuum platform from Config Entry."""
matter = get_matter(hass)
matter.register_platform_handler(Platform.VACUUM, async_add_entities)
platform = entity_platform.async_get_current_platform()
# This will call Entity.async_handle_get_areas
platform.async_register_entity_service(
SERVICE_GET_AREAS,
schema=None,
func="async_handle_get_areas",
supports_response=SupportsResponse.ONLY,
)
# This will call Entity.async_handle_clean_areas
platform.async_register_entity_service(
SERVICE_CLEAN_AREAS,
schema={
vol.Required("areas"): vol.All(cv.ensure_list, [cv.positive_int]),
},
func="async_handle_clean_areas",
supports_response=SupportsResponse.ONLY,
)
# This will call Entity.async_handle_select_areas
platform.async_register_entity_service(
SERVICE_SELECT_AREAS,
schema={
vol.Required("areas"): vol.All(cv.ensure_list, [cv.positive_int]),
},
func="async_handle_select_areas",
supports_response=SupportsResponse.ONLY,
)
class MatterVacuum(MatterEntity, StateVacuumEntity):
@@ -65,9 +105,23 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
_supported_run_modes: (
dict[int, clusters.RvcRunMode.Structs.ModeOptionStruct] | None
) = None
_attr_matter_areas: dict[str, Any] | None = None
_attr_current_area: int | None = None
_attr_current_area_name: str | None = None
_attr_selected_areas: list[int] | None = None
_attr_supported_maps: list[dict[str, Any]] | None = None
entity_description: StateVacuumEntityDescription
_platform_translation_key = "vacuum"
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
return {
ATTR_CURRENT_AREA: self._attr_current_area,
ATTR_CURRENT_AREA_NAME: self._attr_current_area_name,
ATTR_SELECTED_AREAS: self._attr_selected_areas,
}
def _get_run_mode_by_tag(
self, tag: ModeTag
) -> clusters.RvcRunMode.Structs.ModeOptionStruct | None:
@@ -136,10 +190,160 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
"""Pause the cleaning task."""
await self.send_device_command(clusters.RvcOperationalState.Commands.Pause())
def async_get_areas(self, **kwargs: Any) -> dict[str, Any]:
"""Get available area and map IDs from vacuum appliance."""
supported_areas = self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.SupportedAreas
)
if not supported_areas:
raise HomeAssistantError("Can't get areas from the device.")
# Group by area_id: {area_id: {"map_id": ..., "name": ...}}
areas = {}
for area in supported_areas:
area_id = getattr(area, "areaID", None)
map_id = getattr(area, "mapID", None)
location_name = None
area_info = getattr(area, "areaInfo", None)
if area_info is not None:
location_info = getattr(area_info, "locationInfo", None)
if location_info is not None:
location_name = getattr(location_info, "locationName", None)
if area_id is not None:
areas[area_id] = {"map_id": map_id, "name": location_name}
# Optionally, also extract supported maps if available
supported_maps = self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.SupportedMaps
)
maps = []
if supported_maps:
maps = [
{
"map_id": getattr(m, "mapID", None),
"name": getattr(m, "name", None),
}
for m in supported_maps
]
return {
"areas": areas,
"maps": maps,
}
async def async_handle_get_areas(self, **kwargs: Any) -> ServiceResponse:
"""Get available area and map IDs from vacuum appliance."""
# Group by area_id: {area_id: {"map_id": ..., "name": ...}}
areas = {}
if self._attr_matter_areas is not None:
for area in self._attr_matter_areas:
area_id = getattr(area, "areaID", None)
map_id = getattr(area, "mapID", None)
location_name = None
area_info = getattr(area, "areaInfo", None)
if area_info is not None:
location_info = getattr(area_info, "locationInfo", None)
if location_info is not None:
location_name = getattr(location_info, "locationName", None)
if area_id is not None:
if map_id is NullValue:
areas[area_id] = {"name": location_name}
else:
areas[area_id] = {"map_id": map_id, "name": location_name}
# Optionally, also extract supported maps if available
supported_maps = self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.SupportedMaps
)
maps = []
if supported_maps != NullValue: # chip.clusters.Types.Nullable
maps = [
{
"map_id": getattr(m, "mapID", None)
if getattr(m, "mapID", None) != NullValue
else None,
"name": getattr(m, "name", None),
}
for m in supported_maps
]
return cast(
ServiceResponse,
{
"areas": areas,
"maps": maps,
},
)
return None
async def async_handle_select_areas(
self, areas: list[int], **kwargs: Any
) -> ServiceResponse:
"""Select areas to clean."""
selected_areas = areas
# Matter command to the vacuum cleaner to select the areas.
await self.send_device_command(
clusters.ServiceArea.Commands.SelectAreas(newAreas=selected_areas)
)
# Return response indicating selected areas.
return cast(
ServiceResponse, {"status": "areas selected", "areas": selected_areas}
)
async def async_handle_clean_areas(
self, areas: list[int], **kwargs: Any
) -> ServiceResponse:
"""Start cleaning the specified areas."""
# Matter command to the vacuum cleaner to select the areas.
await self.send_device_command(
clusters.ServiceArea.Commands.SelectAreas(newAreas=areas)
)
# Start the vacuum cleaner after selecting areas.
await self.async_start()
# Return response indicating selected areas.
return cast(
ServiceResponse, {"status": "cleaning areas selected", "areas": areas}
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
self._calculate_features()
# ServiceArea: get areas from the device
self._attr_matter_areas = self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.SupportedAreas
)
# optional CurrentArea attribute
# pylint: disable=too-many-nested-blocks
if self.get_matter_attribute_value(clusters.ServiceArea.Attributes.CurrentArea):
current_area = self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.CurrentArea
)
# get areaInfo.locationInfo.locationName for current_area in SupportedAreas list
area_name = None
if self._attr_matter_areas:
for area in self._attr_matter_areas:
if getattr(area, "areaID", None) == current_area:
area_info = getattr(area, "areaInfo", None)
if area_info is not None:
location_info = getattr(area_info, "locationInfo", None)
if location_info is not None:
area_name = getattr(location_info, "locationName", None)
break
self._attr_current_area = current_area
self._attr_current_area_name = area_name
else:
self._attr_current_area = None
self._attr_current_area_name = None
# optional SelectedAreas attribute
if self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.SelectedAreas
):
self._attr_selected_areas = self.get_matter_attribute_value(
clusters.ServiceArea.Attributes.SelectedAreas
)
# derive state from the run mode + operational state
run_mode_raw: int = self.get_matter_attribute_value(
clusters.RvcRunMode.Attributes.CurrentMode
@@ -220,6 +424,10 @@ DISCOVERY_SCHEMAS = [
clusters.RvcRunMode.Attributes.CurrentMode,
clusters.RvcOperationalState.Attributes.OperationalState,
),
optional_attributes=(
clusters.ServiceArea.Attributes.SelectedAreas,
clusters.ServiceArea.Attributes.CurrentArea,
),
device_type=(device_types.RoboticVacuumCleaner,),
allow_none_value=True,
),

View File

@@ -167,263 +167,178 @@ PROCESS_ACTIONS = {
"stop_supercooling": MieleActions.STOP_SUPERCOOL,
}
STATE_PROGRAM_PHASE_WASHING_MACHINE = {
0: "not_running", # Returned by the API when the machine is switched off entirely.
256: "not_running",
257: "pre_wash",
258: "soak",
259: "pre_wash",
260: "main_wash",
261: "rinse",
262: "rinse_hold",
263: "cleaning",
264: "cooling_down",
265: "drain",
266: "spin",
267: "anti_crease",
268: "finished",
269: "venting",
270: "starch_stop",
271: "freshen_up_and_moisten",
272: "steam_smoothing",
279: "hygiene",
280: "drying",
285: "disinfecting",
295: "steam_smoothing",
65535: "not_running", # Seems to be default for some devices.
}
class ProgramPhaseWashingMachine(MieleEnum, missing_to_none=True):
"""Program phase codes for washing machines."""
STATE_PROGRAM_PHASE_TUMBLE_DRYER = {
0: "not_running",
512: "not_running",
513: "program_running",
514: "drying",
515: "machine_iron",
516: "hand_iron_2",
517: "normal",
518: "normal_plus",
519: "cooling_down",
520: "hand_iron_1",
521: "anti_crease",
522: "finished",
523: "extra_dry",
524: "hand_iron",
526: "moisten",
527: "thermo_spin",
528: "timed_drying",
529: "warm_air",
530: "steam_smoothing",
531: "comfort_cooling",
532: "rinse_out_lint",
533: "rinses",
535: "not_running",
534: "smoothing",
536: "not_running",
537: "not_running",
538: "slightly_dry",
539: "safety_cooling",
65535: "not_running",
}
not_running = 0, 256, 65535
pre_wash = 257, 259
soak = 258
main_wash = 260
rinse = 261
rinse_hold = 262
cleaning = 263
cooling_down = 264
drain = 265
spin = 266
anti_crease = 267
finished = 268
venting = 269
starch_stop = 270
freshen_up_and_moisten = 271
steam_smoothing = 272, 295
hygiene = 279
drying = 280
disinfecting = 285
STATE_PROGRAM_PHASE_DISHWASHER = {
1792: "not_running",
1793: "reactivating",
1794: "pre_dishwash",
1795: "main_dishwash",
1796: "rinse",
1797: "interim_rinse",
1798: "final_rinse",
1799: "drying",
1800: "finished",
1801: "pre_dishwash",
65535: "not_running",
}
STATE_PROGRAM_PHASE_OVEN = {
0: "not_running",
3073: "heating_up",
3074: "process_running",
3078: "process_finished",
3084: "energy_save",
65535: "not_running",
}
STATE_PROGRAM_PHASE_WARMING_DRAWER = {
0: "not_running",
3073: "heating_up",
3075: "door_open",
3094: "keeping_warm",
3088: "cooling_down",
65535: "not_running",
}
STATE_PROGRAM_PHASE_MICROWAVE = {
0: "not_running",
3329: "heating",
3330: "process_running",
3334: "process_finished",
3340: "energy_save",
65535: "not_running",
}
STATE_PROGRAM_PHASE_COFFEE_SYSTEM = {
# Coffee system
3073: "heating_up",
4352: "not_running",
4353: "espresso",
4355: "milk_foam",
4361: "dispensing",
4369: "pre_brewing",
4377: "grinding",
4401: "2nd_grinding",
4354: "hot_milk",
4393: "2nd_pre_brewing",
4385: "2nd_espresso",
4404: "dispensing",
4405: "rinse",
65535: "not_running",
}
STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER = {
0: "not_running",
5889: "vacuum_cleaning",
5890: "returning",
5891: "vacuum_cleaning_paused",
5892: "going_to_target_area",
5893: "wheel_lifted", # F1
5894: "dirty_sensors", # F2
5895: "dust_box_missing", # F3
5896: "blocked_drive_wheels", # F4
5897: "blocked_brushes", # F5
5898: "motor_overload", # F6
5899: "internal_fault", # F7
5900: "blocked_front_wheel", # F8
5903: "docked",
5904: "docked",
5910: "remote_controlled",
65535: "not_running",
}
STATE_PROGRAM_PHASE_STEAM_OVEN = {
0: "not_running",
3863: "steam_reduction",
7938: "process_running",
7939: "waiting_for_start",
7940: "heating_up_phase",
7942: "process_finished",
65535: "not_running",
}
class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
"""Program phase codes for tumble dryers."""
not_running = 0, 512, 535, 536, 537, 65535
program_running = 513
drying = 514
machine_iron = 515
hand_iron_2 = 516
normal = 517
normal_plus = 518
cooling_down = 519
hand_iron_1 = 520
anti_crease = 521
finished = 522
extra_dry = 523
hand_iron = 524
moisten = 526
thermo_spin = 527
timed_drying = 528
warm_air = 529
steam_smoothing = 530
comfort_cooling = 531
rinse_out_lint = 532
rinses = 533
smoothing = 534
slightly_dry = 538
safety_cooling = 539
class ProgramPhaseWasherDryer(MieleEnum, missing_to_none=True):
"""Program phase codes for washer/dryer machines."""
not_running = 0, 256, 512, 535, 536, 537, 65535
pre_wash = 257, 259
soak = 258
main_wash = 260
rinse = 261
rinse_hold = 262
cleaning = 263
cooling_down = 264, 519
drain = 265
spin = 266
anti_crease = 267, 521
finished = 268, 522
venting = 269
starch_stop = 270
freshen_up_and_moisten = 271
steam_smoothing = 272, 295, 530
hygiene = 279
drying = 280, 514
disinfecting = 285
program_running = 513
machine_iron = 515
hand_iron_2 = 516
normal = 517
normal_plus = 518
hand_iron_1 = 520
extra_dry = 523
hand_iron = 524
moisten = 526
thermo_spin = 527
timed_drying = 528
warm_air = 529
comfort_cooling = 531
rinse_out_lint = 532
rinses = 533
smoothing = 534
slightly_dry = 538
safety_cooling = 539
class ProgramPhaseDishwasher(MieleEnum, missing_to_none=True):
"""Program phase codes for dishwashers."""
not_running = 0, 1792, 65535
reactivating = 1793
pre_dishwash = 1794, 1801
main_dishwash = 1795
rinse = 1796
interim_rinse = 1797
final_rinse = 1798
drying = 1799
finished = 1800
class ProgramPhaseOven(MieleEnum, missing_to_none=True):
"""Program phase codes for ovens."""
not_running = 0, 65535
heating_up = 3073
process_running = 3074
process_finished = 3078
energy_save = 3084
pre_heating = 3099
class ProgramPhaseWarmingDrawer(MieleEnum, missing_to_none=True):
"""Program phase codes for warming drawers."""
not_running = 0, 65535
heating_up = 3073
door_open = 3075
keeping_warm = 3094
cooling_down = 3088
class ProgramPhaseMicrowave(MieleEnum, missing_to_none=True):
"""Program phase for microwave units."""
not_running = 0, 65535
heating = 3329
process_running = 3330
process_finished = 3334
energy_save = 3340
class ProgramPhaseCoffeeSystem(MieleEnum, missing_to_none=True):
"""Program phase codes for coffee systems."""
not_running = 0, 4352, 65535
heating_up = 3073
espresso = 4353
hot_milk = 4354
milk_foam = 4355
dispensing = 4361, 4404
pre_brewing = 4369
grinding = 4377
second_espresso = 4385
second_pre_brewing = 4393
second_grinding = 4401
rinse = 4405
class ProgramPhaseRobotVacuumCleaner(MieleEnum, missing_to_none=True):
"""Program phase codes for robot vacuum cleaner."""
not_running = 0, 65535
vacuum_cleaning = 5889
returning = 5890
vacuum_cleaning_paused = 5891
going_to_target_area = 5892
wheel_lifted = 5893 # F1
dirty_sensors = 5894 # F2
dust_box_missing = 5895 # F3
blocked_drive_wheels = 5896 # F4
blocked_brushes = 5897 # F5
motor_overload = 5898 # F6
internal_fault = 5899 # F7
blocked_front_wheel = 5900 # F8
docked = 5903, 5904
remote_controlled = 5910
class ProgramPhaseMicrowaveOvenCombo(MieleEnum, missing_to_none=True):
"""Program phase codes for microwave oven combo."""
not_running = 0, 65535
steam_reduction = 3863
process_running = 7938
waiting_for_start = 7939
heating_up_phase = 7940
process_finished = 7942
class ProgramPhaseSteamOven(MieleEnum, missing_to_none=True):
"""Program phase codes for steam ovens."""
not_running = 0, 65535
steam_reduction = 3863
process_running = 7938
waiting_for_start = 7939
heating_up_phase = 7940
process_finished = 7942
class ProgramPhaseSteamOvenCombi(MieleEnum, missing_to_none=True):
"""Program phase codes for steam oven combi."""
not_running = 0, 65535
heating_up = 3073
process_running = 3074, 7938
process_finished = 3078, 7942
energy_save = 3084
pre_heating = 3099
steam_reduction = 3863
waiting_for_start = 7939
heating_up_phase = 7940
class ProgramPhaseSteamOvenMicro(MieleEnum, missing_to_none=True):
"""Program phase codes for steam oven micro."""
not_running = 0, 65535
heating = 3329
process_running = 3330, 7938, 7942
process_finished = 3334
energy_save = 3340
steam_reduction = 3863
waiting_for_start = 7939
heating_up_phase = 7940
PROGRAM_PHASE: dict[int, type[MieleEnum]] = {
MieleAppliance.WASHING_MACHINE: ProgramPhaseWashingMachine,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL: ProgramPhaseWashingMachine,
MieleAppliance.WASHING_MACHINE_PROFESSIONAL: ProgramPhaseWashingMachine,
MieleAppliance.TUMBLE_DRYER: ProgramPhaseTumbleDryer,
MieleAppliance.DRYER_PROFESSIONAL: ProgramPhaseTumbleDryer,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL: ProgramPhaseTumbleDryer,
MieleAppliance.WASHER_DRYER: ProgramPhaseWasherDryer,
MieleAppliance.DISHWASHER: ProgramPhaseDishwasher,
MieleAppliance.DISHWASHER_SEMI_PROFESSIONAL: ProgramPhaseDishwasher,
MieleAppliance.DISHWASHER_PROFESSIONAL: ProgramPhaseDishwasher,
MieleAppliance.OVEN: ProgramPhaseOven,
MieleAppliance.OVEN_MICROWAVE: ProgramPhaseMicrowaveOvenCombo,
MieleAppliance.STEAM_OVEN: ProgramPhaseSteamOven,
MieleAppliance.STEAM_OVEN_COMBI: ProgramPhaseSteamOvenCombi,
MieleAppliance.STEAM_OVEN_MK2: ProgramPhaseSteamOvenCombi,
MieleAppliance.STEAM_OVEN_MICRO: ProgramPhaseSteamOvenMicro,
MieleAppliance.DIALOG_OVEN: ProgramPhaseOven,
MieleAppliance.MICROWAVE: ProgramPhaseMicrowave,
MieleAppliance.COFFEE_SYSTEM: ProgramPhaseCoffeeSystem,
MieleAppliance.ROBOT_VACUUM_CLEANER: ProgramPhaseRobotVacuumCleaner,
MieleAppliance.DISH_WARMER: ProgramPhaseWarmingDrawer,
STATE_PROGRAM_PHASE: dict[int, dict[int, str]] = {
MieleAppliance.WASHING_MACHINE: STATE_PROGRAM_PHASE_WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_SEMI_PROFESSIONAL: STATE_PROGRAM_PHASE_WASHING_MACHINE,
MieleAppliance.WASHING_MACHINE_PROFESSIONAL: STATE_PROGRAM_PHASE_WASHING_MACHINE,
MieleAppliance.TUMBLE_DRYER: STATE_PROGRAM_PHASE_TUMBLE_DRYER,
MieleAppliance.DRYER_PROFESSIONAL: STATE_PROGRAM_PHASE_TUMBLE_DRYER,
MieleAppliance.TUMBLE_DRYER_SEMI_PROFESSIONAL: STATE_PROGRAM_PHASE_TUMBLE_DRYER,
MieleAppliance.WASHER_DRYER: STATE_PROGRAM_PHASE_WASHING_MACHINE
| STATE_PROGRAM_PHASE_TUMBLE_DRYER,
MieleAppliance.DISHWASHER: STATE_PROGRAM_PHASE_DISHWASHER,
MieleAppliance.DISHWASHER_SEMI_PROFESSIONAL: STATE_PROGRAM_PHASE_DISHWASHER,
MieleAppliance.DISHWASHER_PROFESSIONAL: STATE_PROGRAM_PHASE_DISHWASHER,
MieleAppliance.OVEN: STATE_PROGRAM_PHASE_OVEN,
MieleAppliance.OVEN_MICROWAVE: STATE_PROGRAM_PHASE_MICROWAVE,
MieleAppliance.STEAM_OVEN: STATE_PROGRAM_PHASE_STEAM_OVEN,
MieleAppliance.STEAM_OVEN_COMBI: STATE_PROGRAM_PHASE_OVEN
| STATE_PROGRAM_PHASE_STEAM_OVEN,
MieleAppliance.STEAM_OVEN_MICRO: STATE_PROGRAM_PHASE_MICROWAVE
| STATE_PROGRAM_PHASE_STEAM_OVEN,
MieleAppliance.STEAM_OVEN_MK2: STATE_PROGRAM_PHASE_OVEN
| STATE_PROGRAM_PHASE_STEAM_OVEN,
MieleAppliance.DIALOG_OVEN: STATE_PROGRAM_PHASE_OVEN,
MieleAppliance.MICROWAVE: STATE_PROGRAM_PHASE_MICROWAVE,
MieleAppliance.COFFEE_SYSTEM: STATE_PROGRAM_PHASE_COFFEE_SYSTEM,
MieleAppliance.ROBOT_VACUUM_CLEANER: STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER,
MieleAppliance.DISH_WARMER: STATE_PROGRAM_PHASE_WARMING_DRAWER,
}
class StateProgramType(MieleEnum, missing_to_none=True):
class StateProgramType(MieleEnum):
"""Defines program types."""
normal_operation_mode = 0
@@ -431,9 +346,10 @@ class StateProgramType(MieleEnum, missing_to_none=True):
automatic_program = 2
cleaning_care_program = 3
maintenance_program = 4
missing2none = -9999
class StateDryingStep(MieleEnum, missing_to_none=True):
class StateDryingStep(MieleEnum):
"""Defines drying steps."""
extra_dry = 0
@@ -444,6 +360,7 @@ class StateDryingStep(MieleEnum, missing_to_none=True):
hand_iron_2 = 5
machine_iron = 6
smoothing = 7
missing2none = -9999
WASHING_MACHINE_PROGRAM_ID: dict[int, str] = {
@@ -1397,7 +1314,7 @@ STATE_PROGRAM_ID: dict[int, dict[int, str]] = {
}
class PlatePowerStep(MieleEnum, missing_to_none=True):
class PlatePowerStep(MieleEnum):
"""Plate power settings."""
plate_step_0 = 0
@@ -1422,3 +1339,4 @@ class PlatePowerStep(MieleEnum, missing_to_none=True):
plate_step_18 = 18
plate_step_boost = 117, 118, 218
plate_step_boost_2 = 217
missing2none = -9999

View File

@@ -8,7 +8,7 @@
"iot_class": "cloud_push",
"loggers": ["pymiele"],
"quality_scale": "platinum",
"requirements": ["pymiele==0.5.5"],
"requirements": ["pymiele==0.5.4"],
"single_config_entry": true,
"zeroconf": ["_mieleathome._tcp.local."]
}

View File

@@ -35,8 +35,8 @@ from .const import (
COFFEE_SYSTEM_PROFILE,
DISABLED_TEMP_ENTITIES,
DOMAIN,
PROGRAM_PHASE,
STATE_PROGRAM_ID,
STATE_PROGRAM_PHASE,
STATE_STATUS_TAGS,
MieleAppliance,
PlatePowerStep,
@@ -851,36 +851,29 @@ class MieleStatusSensor(MieleSensor):
return True
# Some phases have names that are not valid python identifiers, so we need to translate
# them in order to avoid breaking changes
PROGRAM_PHASE_TRANSLATION = {
"second_espresso": "2nd_espresso",
"second_grinding": "2nd_grinding",
"second_pre_brewing": "2nd_pre_brewing",
}
class MielePhaseSensor(MieleSensor):
"""Representation of the program phase sensor."""
@property
def native_value(self) -> StateType:
"""Return the state of the phase sensor."""
program_phase = PROGRAM_PHASE[self.device.device_type](
"""Return the state of the sensor."""
ret_val = STATE_PROGRAM_PHASE.get(self.device.device_type, {}).get(
self.device.state_program_phase
).name
return (
PROGRAM_PHASE_TRANSLATION.get(program_phase, program_phase)
if program_phase is not None
else None
)
if ret_val is None:
_LOGGER.debug(
"Unknown program phase: %s on device type: %s",
self.device.state_program_phase,
self.device.device_type,
)
return ret_val
@property
def options(self) -> list[str]:
"""Return the options list for the actual device type."""
phases = PROGRAM_PHASE[self.device.device_type].keys()
return sorted([PROGRAM_PHASE_TRANSLATION.get(phase, phase) for phase in phases])
return sorted(
set(STATE_PROGRAM_PHASE.get(self.device.device_type, {}).values())
)
class MieleProgramIdSensor(MieleSensor):

View File

@@ -291,7 +291,6 @@
"not_running": "Not running",
"pre_brewing": "Pre-brewing",
"pre_dishwash": "Pre-cleaning",
"pre_heating": "Pre-heating",
"pre_wash": "Pre-wash",
"process_finished": "Process finished",
"process_running": "Process running",

View File

@@ -64,7 +64,7 @@ PROGRAM_TO_SPEED: dict[int, str] = {
}
class MieleVacuumStateCode(MieleEnum, missing_to_none=True):
class MieleVacuumStateCode(MieleEnum):
"""Define vacuum state codes."""
idle = 0
@@ -82,6 +82,7 @@ class MieleVacuumStateCode(MieleEnum, missing_to_none=True):
blocked_front_wheel = 5900
docked = 5903, 5904
remote_controlled = 5910
missing2none = -9999
SUPPORTED_FEATURES = (

View File

@@ -29,7 +29,7 @@ from .const import (
CONF_SLAVE_COUNT,
CONF_VIRTUAL_COUNT,
)
from .entity import ModbusBaseEntity
from .entity import BasePlatform
from .modbus import ModbusHub
PARALLEL_UPDATES = 1
@@ -59,7 +59,7 @@ async def async_setup_platform(
async_add_entities(sensors)
class ModbusBinarySensor(ModbusBaseEntity, RestoreEntity, BinarySensorEntity):
class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity):
"""Modbus binary sensor."""
def __init__(

View File

@@ -101,7 +101,7 @@ from .const import (
CONF_WRITE_REGISTERS,
DataType,
)
from .entity import ModbusStructEntity
from .entity import BaseStructPlatform
from .modbus import ModbusHub
PARALLEL_UPDATES = 1
@@ -131,7 +131,7 @@ async def async_setup_platform(
async_add_entities(ModbusThermostat(hass, hub, config) for config in climates)
class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
"""Representation of a Modbus Thermostat."""
_attr_supported_features = (

View File

@@ -23,7 +23,7 @@ from .const import (
CONF_STATUS_REGISTER,
CONF_STATUS_REGISTER_TYPE,
)
from .entity import ModbusBaseEntity
from .entity import BasePlatform
from .modbus import ModbusHub
PARALLEL_UPDATES = 1
@@ -42,7 +42,7 @@ async def async_setup_platform(
async_add_entities(ModbusCover(hass, hub, config) for config in covers)
class ModbusCover(ModbusBaseEntity, CoverEntity, RestoreEntity):
class ModbusCover(BasePlatform, CoverEntity, RestoreEntity):
"""Representation of a Modbus cover."""
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE

View File

@@ -68,7 +68,7 @@ from .const import (
from .modbus import ModbusHub
class ModbusBaseEntity(Entity):
class BasePlatform(Entity):
"""Base for readonly platforms."""
_value: str | None = None
@@ -154,7 +154,7 @@ class ModbusBaseEntity(Entity):
)
class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
class BaseStructPlatform(BasePlatform, RestoreEntity):
"""Base class representing a sensor/climate."""
def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None:
@@ -261,7 +261,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
return self.__process_raw_value(val[0])
class ModbusToggleEntity(ModbusBaseEntity, ToggleEntity, RestoreEntity):
class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity):
"""Base class representing a Modbus switch."""
def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None:

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import get_hub
from .const import CONF_FANS
from .entity import ModbusToggleEntity
from .entity import BaseSwitch
from .modbus import ModbusHub
PARALLEL_UPDATES = 1
@@ -31,7 +31,7 @@ async def async_setup_platform(
async_add_entities(ModbusFan(hass, hub, config) for config in fans)
class ModbusFan(ModbusToggleEntity, FanEntity):
class ModbusFan(BaseSwitch, FanEntity):
"""Class representing a Modbus fan."""
def __init__(

View File

@@ -30,7 +30,7 @@ from .const import (
LIGHT_MODBUS_SCALE_MAX,
LIGHT_MODBUS_SCALE_MIN,
)
from .entity import ModbusToggleEntity
from .entity import BaseSwitch
from .modbus import ModbusHub
PARALLEL_UPDATES = 1
@@ -49,7 +49,7 @@ async def async_setup_platform(
async_add_entities(ModbusLight(hass, hub, config) for config in lights)
class ModbusLight(ModbusToggleEntity, LightEntity):
class ModbusLight(BaseSwitch, LightEntity):
"""Class representing a Modbus light."""
def __init__(

Some files were not shown because too many files have changed in this diff Show More