mirror of
https://github.com/home-assistant/core.git
synced 2025-09-21 10:59:32 +00:00
Compare commits
3 Commits
simplify_s
...
mqtt-entit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
be5dfcd06d | ||
![]() |
29bda601cf | ||
![]() |
3f31402d0e |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -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 \
|
||||
|
25
.github/workflows/ci.yaml
vendored
25
.github/workflows/ci.yaml
vendored
@@ -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: |
|
||||
|
@@ -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
20
CODEOWNERS
generated
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ EXCLUDE_FROM_BACKUP = [
|
||||
"tmp_backups/*.tar",
|
||||
"OZW_Log.txt",
|
||||
"tts/*",
|
||||
"ai_task/*",
|
||||
]
|
||||
|
||||
EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
|
@@ -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:
|
||||
|
@@ -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 += (
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
@@ -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
|
||||
)
|
@@ -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,
|
||||
)
|
@@ -1,4 +0,0 @@
|
||||
"""Constants for the Compit integration."""
|
||||
|
||||
DOMAIN = "compit"
|
||||
MANUFACTURER_NAME = "Compit"
|
@@ -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
|
@@ -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"]
|
||||
}
|
@@ -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
|
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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]
|
||||
|
@@ -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] = []
|
||||
|
@@ -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)
|
||||
|
@@ -19,7 +19,6 @@
|
||||
"ssdp",
|
||||
"stream",
|
||||
"sun",
|
||||
"usage_prediction",
|
||||
"usb",
|
||||
"webhook",
|
||||
"zeroconf"
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"downloading_count": {
|
||||
"default": "mdi:download"
|
||||
},
|
||||
"seeding_count": {
|
||||
"default": "mdi:upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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],
|
||||
}
|
@@ -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
|
||||
|
@@ -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,
|
||||
),
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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,
|
||||
)
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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]]
|
||||
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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],
|
||||
}
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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."""
|
||||
|
@@ -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": {
|
||||
|
@@ -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()
|
||||
|
@@ -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": {
|
||||
|
@@ -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": {
|
||||
|
@@ -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(
|
||||
|
@@ -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": {
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@@ -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"
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -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](
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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%]"
|
||||
}
|
||||
}
|
||||
|
@@ -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(),
|
||||
|
@@ -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__,
|
||||
},
|
||||
)
|
||||
|
@@ -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,
|
||||
},
|
||||
)
|
||||
|
@@ -5,4 +5,3 @@ from typing import Final
|
||||
DOMAIN: Final = "lamarzocco"
|
||||
|
||||
CONF_USE_BLUETOOTH: Final = "use_bluetooth"
|
||||
CONF_INSTALLATION_KEY: Final = "installation_key"
|
||||
|
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.1.0"]
|
||||
"requirements": ["pylamarzocco==2.0.11"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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(
|
||||
{
|
||||
|
@@ -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.",
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
homeassistant/components/matter/services.yaml
Normal file
24
homeassistant/components/matter/services.yaml
Normal 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]
|
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
),
|
||||
|
@@ -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
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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):
|
||||
|
@@ -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",
|
||||
|
@@ -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 = (
|
||||
|
@@ -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__(
|
||||
|
@@ -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 = (
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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__(
|
||||
|
@@ -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
Reference in New Issue
Block a user