Compare commits
24 commits
dev
...
vacuum-sta
Author | SHA1 | Date | |
---|---|---|---|
|
7e487f9563 | ||
|
10a6f22add | ||
|
c1581bc18b | ||
|
bb4f13fdb5 | ||
|
3ec0704405 | ||
|
e53d165db5 | ||
|
f832cacb9a | ||
|
6b50da1db2 | ||
|
02f496f465 | ||
|
e4835a31c7 | ||
|
9a9673aaa4 | ||
|
4caaaf86bc | ||
|
3cfef0cc86 | ||
|
aef136449f | ||
|
2bc1b6bddd | ||
|
ae64169b72 | ||
|
82aa779766 | ||
|
1026758abd | ||
|
d638a3b6e3 | ||
|
ea2055c403 | ||
|
8ca4097dda | ||
|
2c3c9f057f | ||
|
76574b5a12 | ||
|
9fd9d1f106 |
184 changed files with 1813 additions and 4103 deletions
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
|
@ -10,7 +10,7 @@ on:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_TYPE: core
|
BUILD_TYPE: core
|
||||||
DEFAULT_PYTHON: "3.13"
|
DEFAULT_PYTHON: "3.12"
|
||||||
PIP_TIMEOUT: 60
|
PIP_TIMEOUT: 60
|
||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
|
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
@ -24,11 +24,11 @@ jobs:
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.27.3
|
uses: github/codeql-action/init@v3.27.1
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.27.3
|
uses: github/codeql-action/analyze@v3.27.1
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|
|
@ -90,7 +90,7 @@ repos:
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
|
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$
|
||||||
- id: hassfest-mypy-config
|
- id: hassfest-mypy-config
|
||||||
name: hassfest-mypy-config
|
name: hassfest-mypy-config
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
||||||
|
|
|
@ -40,8 +40,6 @@ build.json @home-assistant/supervisor
|
||||||
# Integrations
|
# Integrations
|
||||||
/homeassistant/components/abode/ @shred86
|
/homeassistant/components/abode/ @shred86
|
||||||
/tests/components/abode/ @shred86
|
/tests/components/abode/ @shred86
|
||||||
/homeassistant/components/acaia/ @zweckj
|
|
||||||
/tests/components/acaia/ @zweckj
|
|
||||||
/homeassistant/components/accuweather/ @bieniu
|
/homeassistant/components/accuweather/ @bieniu
|
||||||
/tests/components/accuweather/ @bieniu
|
/tests/components/accuweather/ @bieniu
|
||||||
/homeassistant/components/acmeda/ @atmurray
|
/homeassistant/components/acmeda/ @atmurray
|
||||||
|
@ -1346,8 +1344,6 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/siren/ @home-assistant/core @raman325
|
/tests/components/siren/ @home-assistant/core @raman325
|
||||||
/homeassistant/components/sisyphus/ @jkeljo
|
/homeassistant/components/sisyphus/ @jkeljo
|
||||||
/homeassistant/components/sky_hub/ @rogerselwyn
|
/homeassistant/components/sky_hub/ @rogerselwyn
|
||||||
/homeassistant/components/sky_remote/ @dunnmj @saty9
|
|
||||||
/tests/components/sky_remote/ @dunnmj @saty9
|
|
||||||
/homeassistant/components/skybell/ @tkdrob
|
/homeassistant/components/skybell/ @tkdrob
|
||||||
/tests/components/skybell/ @tkdrob
|
/tests/components/skybell/ @tkdrob
|
||||||
/homeassistant/components/slack/ @tkdrob @fletcherau
|
/homeassistant/components/slack/ @tkdrob @fletcherau
|
||||||
|
@ -1489,8 +1485,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/tedee/ @patrickhilker @zweckj
|
/tests/components/tedee/ @patrickhilker @zweckj
|
||||||
/homeassistant/components/tellduslive/ @fredrike
|
/homeassistant/components/tellduslive/ @fredrike
|
||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/template/ @PhracturedBlue @home-assistant/core
|
/homeassistant/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
/tests/components/template/ @PhracturedBlue @home-assistant/core
|
/tests/components/template/ @PhracturedBlue @tetienne @home-assistant/core
|
||||||
/homeassistant/components/tesla_fleet/ @Bre77
|
/homeassistant/components/tesla_fleet/ @Bre77
|
||||||
/tests/components/tesla_fleet/ @Bre77
|
/tests/components/tesla_fleet/ @Bre77
|
||||||
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
/homeassistant/components/tesla_wall_connector/ @einarhauks
|
||||||
|
|
|
@ -55,7 +55,7 @@ RUN \
|
||||||
"armv7") go2rtc_suffix='arm' ;; \
|
"armv7") go2rtc_suffix='arm' ;; \
|
||||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||||
esac \
|
esac \
|
||||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.7/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.6/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||||
&& chmod +x /bin/go2rtc \
|
&& chmod +x /bin/go2rtc \
|
||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
&& go2rtc --version
|
&& go2rtc --version
|
||||||
|
|
10
build.yaml
10
build.yaml
|
@ -1,10 +1,10 @@
|
||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.11.0
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.06.1
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.11.0
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.06.1
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.11.0
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.06.1
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.11.0
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.06.1
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.11.0
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.06.1
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
|
|
@ -515,7 +515,7 @@ async def async_from_config_dict(
|
||||||
issue_registry.async_create_issue(
|
issue_registry.async_create_issue(
|
||||||
hass,
|
hass,
|
||||||
core.DOMAIN,
|
core.DOMAIN,
|
||||||
f"python_version_{required_python_version}",
|
"python_version",
|
||||||
is_fixable=False,
|
is_fixable=False,
|
||||||
severity=issue_registry.IssueSeverity.WARNING,
|
severity=issue_registry.IssueSeverity.WARNING,
|
||||||
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"domain": "sky",
|
|
||||||
"name": "Sky",
|
|
||||||
"integrations": ["sky_hub", "sky_remote"]
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
"""Initialize the Acaia component."""
|
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .coordinator import AcaiaConfigEntry, AcaiaCoordinator
|
|
||||||
|
|
||||||
PLATFORMS = [
|
|
||||||
Platform.BUTTON,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AcaiaConfigEntry) -> bool:
|
|
||||||
"""Set up acaia as config entry."""
|
|
||||||
|
|
||||||
coordinator = AcaiaCoordinator(hass, entry)
|
|
||||||
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: AcaiaConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
@ -1,61 +0,0 @@
|
||||||
"""Button entities for Acaia scales."""
|
|
||||||
|
|
||||||
from collections.abc import Callable, Coroutine
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from aioacaia.acaiascale import AcaiaScale
|
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from .coordinator import AcaiaConfigEntry
|
|
||||||
from .entity import AcaiaEntity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
|
||||||
class AcaiaButtonEntityDescription(ButtonEntityDescription):
|
|
||||||
"""Description for acaia button entities."""
|
|
||||||
|
|
||||||
press_fn: Callable[[AcaiaScale], Coroutine[Any, Any, None]]
|
|
||||||
|
|
||||||
|
|
||||||
BUTTONS: tuple[AcaiaButtonEntityDescription, ...] = (
|
|
||||||
AcaiaButtonEntityDescription(
|
|
||||||
key="tare",
|
|
||||||
translation_key="tare",
|
|
||||||
press_fn=lambda scale: scale.tare(),
|
|
||||||
),
|
|
||||||
AcaiaButtonEntityDescription(
|
|
||||||
key="reset_timer",
|
|
||||||
translation_key="reset_timer",
|
|
||||||
press_fn=lambda scale: scale.reset_timer(),
|
|
||||||
),
|
|
||||||
AcaiaButtonEntityDescription(
|
|
||||||
key="start_stop",
|
|
||||||
translation_key="start_stop",
|
|
||||||
press_fn=lambda scale: scale.start_stop_timer(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AcaiaConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up button entities and services."""
|
|
||||||
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
async_add_entities(AcaiaButton(coordinator, description) for description in BUTTONS)
|
|
||||||
|
|
||||||
|
|
||||||
class AcaiaButton(AcaiaEntity, ButtonEntity):
|
|
||||||
"""Representation of an Acaia button."""
|
|
||||||
|
|
||||||
entity_description: AcaiaButtonEntityDescription
|
|
||||||
|
|
||||||
async def async_press(self) -> None:
|
|
||||||
"""Handle the button press."""
|
|
||||||
await self.entity_description.press_fn(self._scale)
|
|
|
@ -1,149 +0,0 @@
|
||||||
"""Config flow for Acaia integration."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError, AcaiaUnknownDevice
|
|
||||||
from aioacaia.helpers import is_new_scale
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import (
|
|
||||||
BluetoothServiceInfoBleak,
|
|
||||||
async_discovered_service_info,
|
|
||||||
)
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
SelectOptionDict,
|
|
||||||
SelectSelector,
|
|
||||||
SelectSelectorConfig,
|
|
||||||
SelectSelectorMode,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import CONF_IS_NEW_STYLE_SCALE, DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for acaia."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the config flow."""
|
|
||||||
self._discovered: dict[str, Any] = {}
|
|
||||||
self._discovered_devices: dict[str, str] = {}
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle a flow initialized by the user."""
|
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
mac = format_mac(user_input[CONF_ADDRESS])
|
|
||||||
try:
|
|
||||||
is_new_style_scale = await is_new_scale(mac)
|
|
||||||
except AcaiaDeviceNotFound:
|
|
||||||
errors["base"] = "device_not_found"
|
|
||||||
except AcaiaError:
|
|
||||||
_LOGGER.exception("Error occurred while connecting to the scale")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
except AcaiaUnknownDevice:
|
|
||||||
return self.async_abort(reason="unsupported_device")
|
|
||||||
else:
|
|
||||||
await self.async_set_unique_id(mac)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
if not errors:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self._discovered_devices[user_input[CONF_ADDRESS]],
|
|
||||||
data={
|
|
||||||
CONF_ADDRESS: mac,
|
|
||||||
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
for device in async_discovered_service_info(self.hass):
|
|
||||||
self._discovered_devices[device.address] = device.name
|
|
||||||
|
|
||||||
if not self._discovered_devices:
|
|
||||||
return self.async_abort(reason="no_devices_found")
|
|
||||||
|
|
||||||
options = [
|
|
||||||
SelectOptionDict(
|
|
||||||
value=device_mac,
|
|
||||||
label=f"{device_name} ({device_mac})",
|
|
||||||
)
|
|
||||||
for device_mac, device_name in self._discovered_devices.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_ADDRESS): SelectSelector(
|
|
||||||
SelectSelectorConfig(
|
|
||||||
options=options,
|
|
||||||
mode=SelectSelectorMode.DROPDOWN,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_bluetooth(
|
|
||||||
self, discovery_info: BluetoothServiceInfoBleak
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle a discovered Bluetooth device."""
|
|
||||||
|
|
||||||
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address)
|
|
||||||
self._discovered[CONF_NAME] = discovery_info.name
|
|
||||||
|
|
||||||
await self.async_set_unique_id(mac)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._discovered[CONF_IS_NEW_STYLE_SCALE] = await is_new_scale(
|
|
||||||
discovery_info.address
|
|
||||||
)
|
|
||||||
except AcaiaDeviceNotFound:
|
|
||||||
_LOGGER.debug("Device not found during discovery")
|
|
||||||
return self.async_abort(reason="device_not_found")
|
|
||||||
except AcaiaError:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Error occurred while connecting to the scale during discovery",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
return self.async_abort(reason="unknown")
|
|
||||||
except AcaiaUnknownDevice:
|
|
||||||
_LOGGER.debug("Unsupported device during discovery")
|
|
||||||
return self.async_abort(reason="unsupported_device")
|
|
||||||
|
|
||||||
return await self.async_step_bluetooth_confirm()
|
|
||||||
|
|
||||||
async def async_step_bluetooth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle confirmation of Bluetooth discovery."""
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self._discovered[CONF_NAME],
|
|
||||||
data={
|
|
||||||
CONF_ADDRESS: self._discovered[CONF_ADDRESS],
|
|
||||||
CONF_IS_NEW_STYLE_SCALE: self._discovered[CONF_IS_NEW_STYLE_SCALE],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.context["title_placeholders"] = placeholders = {
|
|
||||||
CONF_NAME: self._discovered[CONF_NAME]
|
|
||||||
}
|
|
||||||
|
|
||||||
self._set_confirm_only()
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="bluetooth_confirm",
|
|
||||||
description_placeholders=placeholders,
|
|
||||||
)
|
|
|
@ -1,4 +0,0 @@
|
||||||
"""Constants for component."""
|
|
||||||
|
|
||||||
DOMAIN = "acaia"
|
|
||||||
CONF_IS_NEW_STYLE_SCALE = "is_new_style_scale"
|
|
|
@ -1,86 +0,0 @@
|
||||||
"""Coordinator for Acaia integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from aioacaia.acaiascale import AcaiaScale
|
|
||||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_ADDRESS
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
from .const import CONF_IS_NEW_STYLE_SCALE
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
type AcaiaConfigEntry = ConfigEntry[AcaiaCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
class AcaiaCoordinator(DataUpdateCoordinator[None]):
|
|
||||||
"""Class to handle fetching data from the scale."""
|
|
||||||
|
|
||||||
config_entry: AcaiaConfigEntry
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, entry: AcaiaConfigEntry) -> None:
|
|
||||||
"""Initialize coordinator."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name="acaia coordinator",
|
|
||||||
update_interval=SCAN_INTERVAL,
|
|
||||||
config_entry=entry,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._scale = AcaiaScale(
|
|
||||||
address_or_ble_device=entry.data[CONF_ADDRESS],
|
|
||||||
name=entry.title,
|
|
||||||
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
|
||||||
notify_callback=self.async_update_listeners,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scale(self) -> AcaiaScale:
|
|
||||||
"""Return the scale object."""
|
|
||||||
return self._scale
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
|
||||||
"""Fetch data."""
|
|
||||||
|
|
||||||
# scale is already connected, return
|
|
||||||
if self._scale.connected:
|
|
||||||
return
|
|
||||||
|
|
||||||
# scale is not connected, try to connect
|
|
||||||
try:
|
|
||||||
await self._scale.connect(setup_tasks=False)
|
|
||||||
except (AcaiaDeviceNotFound, AcaiaError, TimeoutError) as ex:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Could not connect to scale: %s, Error: %s",
|
|
||||||
self.config_entry.data[CONF_ADDRESS],
|
|
||||||
ex,
|
|
||||||
)
|
|
||||||
self._scale.device_disconnected_handler(notify=False)
|
|
||||||
return
|
|
||||||
|
|
||||||
# connected, set up background tasks
|
|
||||||
if not self._scale.heartbeat_task or self._scale.heartbeat_task.done():
|
|
||||||
self._scale.heartbeat_task = self.config_entry.async_create_background_task(
|
|
||||||
hass=self.hass,
|
|
||||||
target=self._scale.send_heartbeats(),
|
|
||||||
name="acaia_heartbeat_task",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self._scale.process_queue_task or self._scale.process_queue_task.done():
|
|
||||||
self._scale.process_queue_task = (
|
|
||||||
self.config_entry.async_create_background_task(
|
|
||||||
hass=self.hass,
|
|
||||||
target=self._scale.process_queue(),
|
|
||||||
name="acaia_process_queue_task",
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -1,40 +0,0 @@
|
||||||
"""Base class for Acaia entities."""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import AcaiaCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
|
|
||||||
"""Common elements for all entities."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AcaiaCoordinator,
|
|
||||||
entity_description: EntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
self._scale = coordinator.scale
|
|
||||||
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}"
|
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, self._scale.mac)},
|
|
||||||
manufacturer="Acaia",
|
|
||||||
model=self._scale.model,
|
|
||||||
suggested_area="Kitchen",
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Returns whether entity is available."""
|
|
||||||
return super().available and self._scale.connected
|
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"entity": {
|
|
||||||
"button": {
|
|
||||||
"tare": {
|
|
||||||
"default": "mdi:scale-balance"
|
|
||||||
},
|
|
||||||
"reset_timer": {
|
|
||||||
"default": "mdi:timer-refresh"
|
|
||||||
},
|
|
||||||
"start_stop": {
|
|
||||||
"default": "mdi:timer-play"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"domain": "acaia",
|
|
||||||
"name": "Acaia",
|
|
||||||
"bluetooth": [
|
|
||||||
{
|
|
||||||
"manufacturer_id": 16962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"local_name": "ACAIA*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"local_name": "PYXIS-*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"local_name": "LUNAR-*"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"local_name": "PROCHBT001"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"codeowners": ["@zweckj"],
|
|
||||||
"config_flow": true,
|
|
||||||
"dependencies": ["bluetooth_adapters"],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/acaia",
|
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_push",
|
|
||||||
"loggers": ["aioacaia"],
|
|
||||||
"requirements": ["aioacaia==0.1.6"]
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"flow_title": "{name}",
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
|
||||||
"unsupported_device": "This device is not supported."
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"device_not_found": "Device could not be found.",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"bluetooth_confirm": {
|
|
||||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
|
||||||
"data": {
|
|
||||||
"address": "[%key:common::config_flow::data::device%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"button": {
|
|
||||||
"tare": {
|
|
||||||
"name": "Tare"
|
|
||||||
},
|
|
||||||
"reset_timer": {
|
|
||||||
"name": "Reset timer"
|
|
||||||
},
|
|
||||||
"start_stop": {
|
|
||||||
"name": "Start/stop timer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -11,5 +11,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.9.6"]
|
"requirements": ["aioairzone==0.9.5"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Final, final
|
from typing import Any, Final, final
|
||||||
|
|
||||||
from propcache import cached_property
|
from propcache import cached_property
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -221,15 +221,9 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def state(self) -> str | None:
|
||||||
"""Return the current state."""
|
"""Return the current state."""
|
||||||
if (alarm_state := self.alarm_state) is not None:
|
if (alarm_state := self.alarm_state) is None:
|
||||||
return alarm_state
|
return None
|
||||||
if self._attr_state is not None:
|
return alarm_state
|
||||||
# Backwards compatibility for integrations that set state directly
|
|
||||||
# Should be removed in 2025.11
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert isinstance(self._attr_state, str)
|
|
||||||
return self._attr_state
|
|
||||||
return None
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def alarm_state(self) -> AlarmControlPanelState | None:
|
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||||
|
|
|
@ -436,7 +436,7 @@ class AlexaPowerController(AlexaCapability):
|
||||||
elif self.entity.domain == remote.DOMAIN:
|
elif self.entity.domain == remote.DOMAIN:
|
||||||
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||||
elif self.entity.domain == vacuum.DOMAIN:
|
elif self.entity.domain == vacuum.DOMAIN:
|
||||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
is_on = self.entity.state == vacuum.VacuumActivity.CLEANING
|
||||||
elif self.entity.domain == timer.DOMAIN:
|
elif self.entity.domain == timer.DOMAIN:
|
||||||
is_on = self.entity.state != STATE_IDLE
|
is_on = self.entity.state != STATE_IDLE
|
||||||
elif self.entity.domain == water_heater.DOMAIN:
|
elif self.entity.domain == water_heater.DOMAIN:
|
||||||
|
|
|
@ -32,9 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
||||||
async def async_handle_create_service(call: ServiceCall) -> None:
|
async def async_handle_create_service(call: ServiceCall) -> None:
|
||||||
"""Service handler for creating backups."""
|
"""Service handler for creating backups."""
|
||||||
await backup_manager.async_create_backup(on_progress=None)
|
await backup_manager.async_create_backup()
|
||||||
if backup_task := backup_manager.backup_task:
|
|
||||||
await backup_task
|
|
||||||
|
|
||||||
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
|
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
|
||||||
|
|
||||||
|
|
|
@ -2,26 +2,23 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from aiohttp import BodyPartReader
|
|
||||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||||
from aiohttp.web import FileResponse, Request, Response
|
from aiohttp.web import FileResponse, Request, Response
|
||||||
|
|
||||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
|
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .const import DATA_MANAGER
|
from .const import DOMAIN
|
||||||
|
from .manager import BaseBackupManager
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_register_http_views(hass: HomeAssistant) -> None:
|
def async_register_http_views(hass: HomeAssistant) -> None:
|
||||||
"""Register the http views."""
|
"""Register the http views."""
|
||||||
hass.http.register_view(DownloadBackupView)
|
hass.http.register_view(DownloadBackupView)
|
||||||
hass.http.register_view(UploadBackupView)
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadBackupView(HomeAssistantView):
|
class DownloadBackupView(HomeAssistantView):
|
||||||
|
@ -39,7 +36,7 @@ class DownloadBackupView(HomeAssistantView):
|
||||||
if not request["hass_user"].is_admin:
|
if not request["hass_user"].is_admin:
|
||||||
return Response(status=HTTPStatus.UNAUTHORIZED)
|
return Response(status=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
manager = request.app[KEY_HASS].data[DATA_MANAGER]
|
manager: BaseBackupManager = request.app[KEY_HASS].data[DOMAIN]
|
||||||
backup = await manager.async_get_backup(slug=slug)
|
backup = await manager.async_get_backup(slug=slug)
|
||||||
|
|
||||||
if backup is None or not backup.path.exists():
|
if backup is None or not backup.path.exists():
|
||||||
|
@ -51,29 +48,3 @@ class DownloadBackupView(HomeAssistantView):
|
||||||
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
|
CONTENT_DISPOSITION: f"attachment; filename={slugify(backup.name)}.tar"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UploadBackupView(HomeAssistantView):
|
|
||||||
"""Generate backup view."""
|
|
||||||
|
|
||||||
url = "/api/backup/upload"
|
|
||||||
name = "api:backup:upload"
|
|
||||||
|
|
||||||
@require_admin
|
|
||||||
async def post(self, request: Request) -> Response:
|
|
||||||
"""Upload a backup file."""
|
|
||||||
manager = request.app[KEY_HASS].data[DATA_MANAGER]
|
|
||||||
reader = await request.multipart()
|
|
||||||
contents = cast(BodyPartReader, await reader.next())
|
|
||||||
|
|
||||||
try:
|
|
||||||
await manager.async_receive_backup(contents=contents)
|
|
||||||
except OSError as err:
|
|
||||||
return Response(
|
|
||||||
body=f"Can't write backup file {err}",
|
|
||||||
status=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
return Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
return Response(status=HTTPStatus.CREATED)
|
|
||||||
|
|
|
@ -4,21 +4,16 @@ from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import SimpleQueue
|
|
||||||
import shutil
|
|
||||||
import tarfile
|
import tarfile
|
||||||
from tarfile import TarError
|
from tarfile import TarError
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
import time
|
import time
|
||||||
from typing import Any, Protocol, cast
|
from typing import Any, Protocol, cast
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from securetar import SecureTarFile, atomic_contents_add
|
from securetar import SecureTarFile, atomic_contents_add
|
||||||
|
|
||||||
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
|
from homeassistant.backup_restore import RESTORE_BACKUP_FILE
|
||||||
|
@ -35,13 +30,6 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
|
||||||
BUF_SIZE = 2**20 * 4 # 4MB
|
BUF_SIZE = 2**20 * 4 # 4MB
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class NewBackup:
|
|
||||||
"""New backup class."""
|
|
||||||
|
|
||||||
slug: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class Backup:
|
class Backup:
|
||||||
"""Backup class."""
|
"""Backup class."""
|
||||||
|
@ -57,15 +45,6 @@ class Backup:
|
||||||
return {**asdict(self), "path": self.path.as_posix()}
|
return {**asdict(self), "path": self.path.as_posix()}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class BackupProgress:
|
|
||||||
"""Backup progress class."""
|
|
||||||
|
|
||||||
done: bool
|
|
||||||
stage: str | None
|
|
||||||
success: bool | None
|
|
||||||
|
|
||||||
|
|
||||||
class BackupPlatformProtocol(Protocol):
|
class BackupPlatformProtocol(Protocol):
|
||||||
"""Define the format that backup platforms can have."""
|
"""Define the format that backup platforms can have."""
|
||||||
|
|
||||||
|
@ -82,7 +61,7 @@ class BaseBackupManager(abc.ABC):
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the backup manager."""
|
"""Initialize the backup manager."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.backup_task: asyncio.Task | None = None
|
self.backing_up = False
|
||||||
self.backups: dict[str, Backup] = {}
|
self.backups: dict[str, Backup] = {}
|
||||||
self.loaded_platforms = False
|
self.loaded_platforms = False
|
||||||
self.platforms: dict[str, BackupPlatformProtocol] = {}
|
self.platforms: dict[str, BackupPlatformProtocol] = {}
|
||||||
|
@ -150,12 +129,7 @@ class BaseBackupManager(abc.ABC):
|
||||||
"""Restore a backup."""
|
"""Restore a backup."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def async_create_backup(
|
async def async_create_backup(self, **kwargs: Any) -> Backup:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
on_progress: Callable[[BackupProgress], None] | None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> NewBackup:
|
|
||||||
"""Generate a backup."""
|
"""Generate a backup."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -173,15 +147,6 @@ class BaseBackupManager(abc.ABC):
|
||||||
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
|
async def async_remove_backup(self, *, slug: str, **kwargs: Any) -> None:
|
||||||
"""Remove a backup."""
|
"""Remove a backup."""
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
async def async_receive_backup(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
contents: aiohttp.BodyPartReader,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""Receive and store a backup file from upload."""
|
|
||||||
|
|
||||||
|
|
||||||
class BackupManager(BaseBackupManager):
|
class BackupManager(BaseBackupManager):
|
||||||
"""Backup manager for the Backup integration."""
|
"""Backup manager for the Backup integration."""
|
||||||
|
@ -257,93 +222,17 @@ class BackupManager(BaseBackupManager):
|
||||||
LOGGER.debug("Removed backup located at %s", backup.path)
|
LOGGER.debug("Removed backup located at %s", backup.path)
|
||||||
self.backups.pop(slug)
|
self.backups.pop(slug)
|
||||||
|
|
||||||
async def async_receive_backup(
|
async def async_create_backup(self, **kwargs: Any) -> Backup:
|
||||||
self,
|
|
||||||
*,
|
|
||||||
contents: aiohttp.BodyPartReader,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""Receive and store a backup file from upload."""
|
|
||||||
queue: SimpleQueue[tuple[bytes, asyncio.Future[None] | None] | None] = (
|
|
||||||
SimpleQueue()
|
|
||||||
)
|
|
||||||
temp_dir_handler = await self.hass.async_add_executor_job(TemporaryDirectory)
|
|
||||||
target_temp_file = Path(
|
|
||||||
temp_dir_handler.name, contents.filename or "backup.tar"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _sync_queue_consumer() -> None:
|
|
||||||
with target_temp_file.open("wb") as file_handle:
|
|
||||||
while True:
|
|
||||||
if (_chunk_future := queue.get()) is None:
|
|
||||||
break
|
|
||||||
_chunk, _future = _chunk_future
|
|
||||||
if _future is not None:
|
|
||||||
self.hass.loop.call_soon_threadsafe(_future.set_result, None)
|
|
||||||
file_handle.write(_chunk)
|
|
||||||
|
|
||||||
fut: asyncio.Future[None] | None = None
|
|
||||||
try:
|
|
||||||
fut = self.hass.async_add_executor_job(_sync_queue_consumer)
|
|
||||||
megabytes_sending = 0
|
|
||||||
while chunk := await contents.read_chunk(BUF_SIZE):
|
|
||||||
megabytes_sending += 1
|
|
||||||
if megabytes_sending % 5 != 0:
|
|
||||||
queue.put_nowait((chunk, None))
|
|
||||||
continue
|
|
||||||
|
|
||||||
chunk_future = self.hass.loop.create_future()
|
|
||||||
queue.put_nowait((chunk, chunk_future))
|
|
||||||
await asyncio.wait(
|
|
||||||
(fut, chunk_future),
|
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
|
||||||
)
|
|
||||||
if fut.done():
|
|
||||||
# The executor job failed
|
|
||||||
break
|
|
||||||
|
|
||||||
queue.put_nowait(None) # terminate queue consumer
|
|
||||||
finally:
|
|
||||||
if fut is not None:
|
|
||||||
await fut
|
|
||||||
|
|
||||||
def _move_and_cleanup() -> None:
|
|
||||||
shutil.move(target_temp_file, self.backup_dir / target_temp_file.name)
|
|
||||||
temp_dir_handler.cleanup()
|
|
||||||
|
|
||||||
await self.hass.async_add_executor_job(_move_and_cleanup)
|
|
||||||
await self.load_backups()
|
|
||||||
|
|
||||||
async def async_create_backup(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
on_progress: Callable[[BackupProgress], None] | None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> NewBackup:
|
|
||||||
"""Generate a backup."""
|
"""Generate a backup."""
|
||||||
if self.backup_task:
|
if self.backing_up:
|
||||||
raise HomeAssistantError("Backup already in progress")
|
raise HomeAssistantError("Backup already in progress")
|
||||||
backup_name = f"Core {HAVERSION}"
|
|
||||||
date_str = dt_util.now().isoformat()
|
|
||||||
slug = _generate_slug(date_str, backup_name)
|
|
||||||
self.backup_task = self.hass.async_create_task(
|
|
||||||
self._async_create_backup(backup_name, date_str, slug, on_progress),
|
|
||||||
name="backup_manager_create_backup",
|
|
||||||
eager_start=False, # To ensure the task is not started before we return
|
|
||||||
)
|
|
||||||
return NewBackup(slug=slug)
|
|
||||||
|
|
||||||
async def _async_create_backup(
|
|
||||||
self,
|
|
||||||
backup_name: str,
|
|
||||||
date_str: str,
|
|
||||||
slug: str,
|
|
||||||
on_progress: Callable[[BackupProgress], None] | None,
|
|
||||||
) -> Backup:
|
|
||||||
"""Generate a backup."""
|
|
||||||
success = False
|
|
||||||
try:
|
try:
|
||||||
|
self.backing_up = True
|
||||||
await self.async_pre_backup_actions()
|
await self.async_pre_backup_actions()
|
||||||
|
backup_name = f"Core {HAVERSION}"
|
||||||
|
date_str = dt_util.now().isoformat()
|
||||||
|
slug = _generate_slug(date_str, backup_name)
|
||||||
|
|
||||||
backup_data = {
|
backup_data = {
|
||||||
"slug": slug,
|
"slug": slug,
|
||||||
|
@ -370,12 +259,9 @@ class BackupManager(BaseBackupManager):
|
||||||
if self.loaded_backups:
|
if self.loaded_backups:
|
||||||
self.backups[slug] = backup
|
self.backups[slug] = backup
|
||||||
LOGGER.debug("Generated new backup with slug %s", slug)
|
LOGGER.debug("Generated new backup with slug %s", slug)
|
||||||
success = True
|
|
||||||
return backup
|
return backup
|
||||||
finally:
|
finally:
|
||||||
if on_progress:
|
self.backing_up = False
|
||||||
on_progress(BackupProgress(done=True, stage=None, success=success))
|
|
||||||
self.backup_task = None
|
|
||||||
await self.async_post_backup_actions()
|
await self.async_post_backup_actions()
|
||||||
|
|
||||||
def _mkdir_and_generate_backup_contents(
|
def _mkdir_and_generate_backup_contents(
|
||||||
|
|
|
@ -8,7 +8,6 @@ from homeassistant.components import websocket_api
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import DATA_MANAGER, LOGGER
|
from .const import DATA_MANAGER, LOGGER
|
||||||
from .manager import BackupProgress
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -41,7 +40,7 @@ async def handle_info(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
{
|
{
|
||||||
"backups": list(backups.values()),
|
"backups": list(backups.values()),
|
||||||
"backing_up": manager.backup_task is not None,
|
"backing_up": manager.backing_up,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -114,11 +113,7 @@ async def handle_create(
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Generate a backup."""
|
"""Generate a backup."""
|
||||||
|
backup = await hass.data[DATA_MANAGER].async_create_backup()
|
||||||
def on_progress(progress: BackupProgress) -> None:
|
|
||||||
connection.send_message(websocket_api.event_message(msg["id"], progress))
|
|
||||||
|
|
||||||
backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress)
|
|
||||||
connection.send_result(msg["id"], backup)
|
connection.send_result(msg["id"], backup)
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,6 +127,7 @@ async def handle_backup_start(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Backup start notification."""
|
"""Backup start notification."""
|
||||||
manager = hass.data[DATA_MANAGER]
|
manager = hass.data[DATA_MANAGER]
|
||||||
|
manager.backing_up = True
|
||||||
LOGGER.debug("Backup start notification")
|
LOGGER.debug("Backup start notification")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -153,6 +149,7 @@ async def handle_backup_end(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Backup end notification."""
|
"""Backup end notification."""
|
||||||
manager = hass.data[DATA_MANAGER]
|
manager = hass.data[DATA_MANAGER]
|
||||||
|
manager.backing_up = False
|
||||||
LOGGER.debug("Backup end notification")
|
LOGGER.debug("Backup end notification")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiostreammagic"],
|
"loggers": ["aiostreammagic"],
|
||||||
"requirements": ["aiostreammagic==2.8.5"],
|
"requirements": ["aiostreammagic==2.8.4"],
|
||||||
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
|
"zeroconf": ["_stream-magic._tcp.local.", "_smoip._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,13 +51,8 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
|
||||||
CambridgeAudioSelectEntityDescription(
|
CambridgeAudioSelectEntityDescription(
|
||||||
key="display_brightness",
|
key="display_brightness",
|
||||||
translation_key="display_brightness",
|
translation_key="display_brightness",
|
||||||
options=[
|
options=[x.value for x in DisplayBrightness],
|
||||||
DisplayBrightness.BRIGHT.value,
|
|
||||||
DisplayBrightness.DIM.value,
|
|
||||||
DisplayBrightness.OFF.value,
|
|
||||||
],
|
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
load_fn=lambda client: client.display.brightness != DisplayBrightness.NONE,
|
|
||||||
value_fn=lambda client: client.display.brightness,
|
value_fn=lambda client: client.display.brightness,
|
||||||
set_value_fn=lambda client, value: client.set_display_brightness(
|
set_value_fn=lambda client, value: client.set_display_brightness(
|
||||||
DisplayBrightness(value)
|
DisplayBrightness(value)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable, Iterable
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from functools import cache, partial, wraps
|
from functools import cache, partial
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Protocol
|
from typing import TYPE_CHECKING, Any, Protocol
|
||||||
|
|
||||||
|
@ -205,49 +205,6 @@ async def _async_refresh_providers(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type WsCommandWithCamera = Callable[
|
|
||||||
[websocket_api.ActiveConnection, dict[str, Any], Camera],
|
|
||||||
Awaitable[None],
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def require_webrtc_support(
|
|
||||||
error_code: str,
|
|
||||||
) -> Callable[[WsCommandWithCamera], websocket_api.AsyncWebSocketCommandHandler]:
|
|
||||||
"""Validate that the camera supports WebRTC."""
|
|
||||||
|
|
||||||
def decorate(
|
|
||||||
func: WsCommandWithCamera,
|
|
||||||
) -> websocket_api.AsyncWebSocketCommandHandler:
|
|
||||||
"""Decorate func."""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
async def validate(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
connection: websocket_api.ActiveConnection,
|
|
||||||
msg: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
"""Validate that the camera supports WebRTC."""
|
|
||||||
entity_id = msg["entity_id"]
|
|
||||||
camera = get_camera_from_entity_id(hass, entity_id)
|
|
||||||
if camera.frontend_stream_type != StreamType.WEB_RTC:
|
|
||||||
connection.send_error(
|
|
||||||
msg["id"],
|
|
||||||
error_code,
|
|
||||||
(
|
|
||||||
"Camera does not support WebRTC,"
|
|
||||||
f" frontend_stream_type={camera.frontend_stream_type}"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
await func(connection, msg, camera)
|
|
||||||
|
|
||||||
return validate
|
|
||||||
|
|
||||||
return decorate
|
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
{
|
{
|
||||||
vol.Required("type"): "camera/webrtc/offer",
|
vol.Required("type"): "camera/webrtc/offer",
|
||||||
|
@ -256,9 +213,8 @@ def require_webrtc_support(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@require_webrtc_support("webrtc_offer_failed")
|
|
||||||
async def ws_webrtc_offer(
|
async def ws_webrtc_offer(
|
||||||
connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle the signal path for a WebRTC stream.
|
"""Handle the signal path for a WebRTC stream.
|
||||||
|
|
||||||
|
@ -270,7 +226,20 @@ async def ws_webrtc_offer(
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
|
entity_id = msg["entity_id"]
|
||||||
offer = msg["offer"]
|
offer = msg["offer"]
|
||||||
|
camera = get_camera_from_entity_id(hass, entity_id)
|
||||||
|
if camera.frontend_stream_type != StreamType.WEB_RTC:
|
||||||
|
connection.send_error(
|
||||||
|
msg["id"],
|
||||||
|
"webrtc_offer_failed",
|
||||||
|
(
|
||||||
|
"Camera does not support WebRTC,"
|
||||||
|
f" frontend_stream_type={camera.frontend_stream_type}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
session_id = ulid()
|
session_id = ulid()
|
||||||
connection.subscriptions[msg["id"]] = partial(
|
connection.subscriptions[msg["id"]] = partial(
|
||||||
camera.close_webrtc_session, session_id
|
camera.close_webrtc_session, session_id
|
||||||
|
@ -309,11 +278,23 @@ async def ws_webrtc_offer(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@require_webrtc_support("webrtc_get_client_config_failed")
|
|
||||||
async def ws_get_client_config(
|
async def ws_get_client_config(
|
||||||
connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get WebRTC client config websocket command."""
|
"""Handle get WebRTC client config websocket command."""
|
||||||
|
entity_id = msg["entity_id"]
|
||||||
|
camera = get_camera_from_entity_id(hass, entity_id)
|
||||||
|
if camera.frontend_stream_type != StreamType.WEB_RTC:
|
||||||
|
connection.send_error(
|
||||||
|
msg["id"],
|
||||||
|
"webrtc_get_client_config_failed",
|
||||||
|
(
|
||||||
|
"Camera does not support WebRTC,"
|
||||||
|
f" frontend_stream_type={camera.frontend_stream_type}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
config = camera.async_get_webrtc_client_configuration().to_frontend_dict()
|
config = camera.async_get_webrtc_client_configuration().to_frontend_dict()
|
||||||
connection.send_result(
|
connection.send_result(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
|
@ -330,11 +311,23 @@ async def ws_get_client_config(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
@require_webrtc_support("webrtc_candidate_failed")
|
|
||||||
async def ws_candidate(
|
async def ws_candidate(
|
||||||
connection: websocket_api.ActiveConnection, msg: dict[str, Any], camera: Camera
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle WebRTC candidate websocket command."""
|
"""Handle WebRTC candidate websocket command."""
|
||||||
|
entity_id = msg["entity_id"]
|
||||||
|
camera = get_camera_from_entity_id(hass, entity_id)
|
||||||
|
if camera.frontend_stream_type != StreamType.WEB_RTC:
|
||||||
|
connection.send_error(
|
||||||
|
msg["id"],
|
||||||
|
"webrtc_candidate_failed",
|
||||||
|
(
|
||||||
|
"Camera does not support WebRTC,"
|
||||||
|
f" frontend_stream_type={camera.frontend_stream_type}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await camera.async_on_webrtc_candidate(
|
await camera.async_on_webrtc_candidate(
|
||||||
msg["session_id"], RTCIceCandidate(msg["candidate"])
|
msg["session_id"], RTCIceCandidate(msg["candidate"])
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,11 +16,11 @@ from hassil.expression import Expression, ListReference, Sequence
|
||||||
from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
|
from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
|
||||||
from hassil.recognize import (
|
from hassil.recognize import (
|
||||||
MISSING_ENTITY,
|
MISSING_ENTITY,
|
||||||
|
MatchEntity,
|
||||||
RecognizeResult,
|
RecognizeResult,
|
||||||
|
UnmatchedTextEntity,
|
||||||
recognize_all,
|
recognize_all,
|
||||||
recognize_best,
|
|
||||||
)
|
)
|
||||||
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
|
||||||
from hassil.util import merge_dict
|
from hassil.util import merge_dict
|
||||||
from home_assistant_intents import ErrorKey, get_intents, get_languages
|
from home_assistant_intents import ErrorKey, get_intents, get_languages
|
||||||
import yaml
|
import yaml
|
||||||
|
@ -499,7 +499,6 @@ class DefaultAgent(ConversationEntity):
|
||||||
maybe_result: RecognizeResult | None = None
|
maybe_result: RecognizeResult | None = None
|
||||||
best_num_matched_entities = 0
|
best_num_matched_entities = 0
|
||||||
best_num_unmatched_entities = 0
|
best_num_unmatched_entities = 0
|
||||||
best_num_unmatched_ranges = 0
|
|
||||||
for result in recognize_all(
|
for result in recognize_all(
|
||||||
user_input.text,
|
user_input.text,
|
||||||
lang_intents.intents,
|
lang_intents.intents,
|
||||||
|
@ -518,14 +517,10 @@ class DefaultAgent(ConversationEntity):
|
||||||
num_matched_entities += 1
|
num_matched_entities += 1
|
||||||
|
|
||||||
num_unmatched_entities = 0
|
num_unmatched_entities = 0
|
||||||
num_unmatched_ranges = 0
|
|
||||||
for unmatched_entity in result.unmatched_entities_list:
|
for unmatched_entity in result.unmatched_entities_list:
|
||||||
if isinstance(unmatched_entity, UnmatchedTextEntity):
|
if isinstance(unmatched_entity, UnmatchedTextEntity):
|
||||||
if unmatched_entity.text != MISSING_ENTITY:
|
if unmatched_entity.text != MISSING_ENTITY:
|
||||||
num_unmatched_entities += 1
|
num_unmatched_entities += 1
|
||||||
elif isinstance(unmatched_entity, UnmatchedRangeEntity):
|
|
||||||
num_unmatched_ranges += 1
|
|
||||||
num_unmatched_entities += 1
|
|
||||||
else:
|
else:
|
||||||
num_unmatched_entities += 1
|
num_unmatched_entities += 1
|
||||||
|
|
||||||
|
@ -537,24 +532,15 @@ class DefaultAgent(ConversationEntity):
|
||||||
(num_matched_entities == best_num_matched_entities)
|
(num_matched_entities == best_num_matched_entities)
|
||||||
and (num_unmatched_entities < best_num_unmatched_entities)
|
and (num_unmatched_entities < best_num_unmatched_entities)
|
||||||
)
|
)
|
||||||
or (
|
|
||||||
# Prefer unmatched ranges
|
|
||||||
(num_matched_entities == best_num_matched_entities)
|
|
||||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
|
||||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
|
||||||
)
|
|
||||||
or (
|
or (
|
||||||
# More literal text matched
|
# More literal text matched
|
||||||
(num_matched_entities == best_num_matched_entities)
|
(num_matched_entities == best_num_matched_entities)
|
||||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
|
||||||
and (result.text_chunks_matched > maybe_result.text_chunks_matched)
|
and (result.text_chunks_matched > maybe_result.text_chunks_matched)
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
# Prefer match failures with entities
|
# Prefer match failures with entities
|
||||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
|
||||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
|
||||||
and (
|
and (
|
||||||
("name" in result.entities)
|
("name" in result.entities)
|
||||||
or ("name" in result.unmatched_entities)
|
or ("name" in result.unmatched_entities)
|
||||||
|
@ -564,7 +550,6 @@ class DefaultAgent(ConversationEntity):
|
||||||
maybe_result = result
|
maybe_result = result
|
||||||
best_num_matched_entities = num_matched_entities
|
best_num_matched_entities = num_matched_entities
|
||||||
best_num_unmatched_entities = num_unmatched_entities
|
best_num_unmatched_entities = num_unmatched_entities
|
||||||
best_num_unmatched_ranges = num_unmatched_ranges
|
|
||||||
|
|
||||||
return maybe_result
|
return maybe_result
|
||||||
|
|
||||||
|
@ -577,15 +562,76 @@ class DefaultAgent(ConversationEntity):
|
||||||
language: str,
|
language: str,
|
||||||
) -> RecognizeResult | None:
|
) -> RecognizeResult | None:
|
||||||
"""Search intents for a strict match to user input."""
|
"""Search intents for a strict match to user input."""
|
||||||
return recognize_best(
|
custom_found = False
|
||||||
|
name_found = False
|
||||||
|
best_results: list[RecognizeResult] = []
|
||||||
|
best_name_quality: int | None = None
|
||||||
|
best_text_chunks_matched: int | None = None
|
||||||
|
for result in recognize_all(
|
||||||
user_input.text,
|
user_input.text,
|
||||||
lang_intents.intents,
|
lang_intents.intents,
|
||||||
slot_lists=slot_lists,
|
slot_lists=slot_lists,
|
||||||
intent_context=intent_context,
|
intent_context=intent_context,
|
||||||
language=language,
|
language=language,
|
||||||
best_metadata_key=METADATA_CUSTOM_SENTENCE,
|
):
|
||||||
best_slot_name="name",
|
# Prioritize user intents
|
||||||
)
|
is_custom = (
|
||||||
|
result.intent_metadata is not None
|
||||||
|
and result.intent_metadata.get(METADATA_CUSTOM_SENTENCE)
|
||||||
|
)
|
||||||
|
|
||||||
|
if custom_found and not is_custom:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not custom_found and is_custom:
|
||||||
|
custom_found = True
|
||||||
|
# Clear builtin results
|
||||||
|
name_found = False
|
||||||
|
best_results = []
|
||||||
|
best_name_quality = None
|
||||||
|
best_text_chunks_matched = None
|
||||||
|
|
||||||
|
# Prioritize results with a "name" slot
|
||||||
|
name = result.entities.get("name")
|
||||||
|
is_name = name and not name.is_wildcard
|
||||||
|
|
||||||
|
if name_found and not is_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not name_found and is_name:
|
||||||
|
name_found = True
|
||||||
|
# Clear non-name results
|
||||||
|
best_results = []
|
||||||
|
best_text_chunks_matched = None
|
||||||
|
|
||||||
|
if is_name:
|
||||||
|
# Prioritize results with a better "name" slot
|
||||||
|
name_quality = len(cast(MatchEntity, name).value.split())
|
||||||
|
if (best_name_quality is None) or (name_quality > best_name_quality):
|
||||||
|
best_name_quality = name_quality
|
||||||
|
# Clear worse name results
|
||||||
|
best_results = []
|
||||||
|
best_text_chunks_matched = None
|
||||||
|
elif name_quality < best_name_quality:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prioritize results with more literal text
|
||||||
|
# This causes wildcards to match last.
|
||||||
|
if (best_text_chunks_matched is None) or (
|
||||||
|
result.text_chunks_matched > best_text_chunks_matched
|
||||||
|
):
|
||||||
|
best_results = [result]
|
||||||
|
best_text_chunks_matched = result.text_chunks_matched
|
||||||
|
elif result.text_chunks_matched == best_text_chunks_matched:
|
||||||
|
# Accumulate results with the same number of literal text matched.
|
||||||
|
# We will resolve the ambiguity below.
|
||||||
|
best_results.append(result)
|
||||||
|
|
||||||
|
if best_results:
|
||||||
|
# Successful strict match
|
||||||
|
return best_results[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def _build_speech(
|
async def _build_speech(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -6,8 +6,12 @@ from collections.abc import Iterable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from hassil.recognize import MISSING_ENTITY, RecognizeResult
|
from hassil.recognize import (
|
||||||
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
MISSING_ENTITY,
|
||||||
|
RecognizeResult,
|
||||||
|
UnmatchedRangeEntity,
|
||||||
|
UnmatchedTextEntity,
|
||||||
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import http, websocket_api
|
from homeassistant.components import http, websocket_api
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==2.0.1", "home-assistant-intents==2024.11.13"]
|
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from hassil.recognize import RecognizeResult
|
from hassil.recognize import PUNCTUATION, RecognizeResult
|
||||||
from hassil.util import PUNCTUATION_ALL
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
|
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
|
||||||
|
@ -21,7 +20,7 @@ from .const import DATA_DEFAULT_ENTITY, DOMAIN
|
||||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||||
"""Validate result does not contain punctuation."""
|
"""Validate result does not contain punctuation."""
|
||||||
for sentence in value:
|
for sentence in value:
|
||||||
if PUNCTUATION_ALL.search(sentence):
|
if PUNCTUATION.search(sentence):
|
||||||
raise vol.Invalid("sentence should not contain punctuation")
|
raise vol.Invalid("sentence should not contain punctuation")
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -7,12 +7,8 @@ from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
ATTR_CLEANED_AREA,
|
ATTR_CLEANED_AREA,
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -91,16 +87,11 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||||
"""Initialize the vacuum."""
|
"""Initialize the vacuum."""
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
self._attr_supported_features = supported_features
|
self._attr_supported_features = supported_features
|
||||||
self._state = STATE_DOCKED
|
self._attr_activity = VacuumActivity.DOCKED
|
||||||
self._fan_speed = FAN_SPEEDS[1]
|
self._fan_speed = FAN_SPEEDS[1]
|
||||||
self._cleaned_area: float = 0
|
self._cleaned_area: float = 0
|
||||||
self._battery_level = 100
|
self._battery_level = 100
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> str:
|
|
||||||
"""Return the current state of the vacuum."""
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def battery_level(self) -> int:
|
def battery_level(self) -> int:
|
||||||
"""Return the current battery level of the vacuum."""
|
"""Return the current battery level of the vacuum."""
|
||||||
|
@ -123,33 +114,33 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Start or resume the cleaning task."""
|
"""Start or resume the cleaning task."""
|
||||||
if self._state != STATE_CLEANING:
|
if self._attr_activity != VacuumActivity.CLEANING:
|
||||||
self._state = STATE_CLEANING
|
self._attr_activity = VacuumActivity.CLEANING
|
||||||
self._cleaned_area += 1.32
|
self._cleaned_area += 1.32
|
||||||
self._battery_level -= 1
|
self._battery_level -= 1
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def pause(self) -> None:
|
def pause(self) -> None:
|
||||||
"""Pause the cleaning task."""
|
"""Pause the cleaning task."""
|
||||||
if self._state == STATE_CLEANING:
|
if self._attr_activity == VacuumActivity.CLEANING:
|
||||||
self._state = STATE_PAUSED
|
self._attr_activity = VacuumActivity.PAUSED
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def stop(self, **kwargs: Any) -> None:
|
def stop(self, **kwargs: Any) -> None:
|
||||||
"""Stop the cleaning task, do not return to dock."""
|
"""Stop the cleaning task, do not return to dock."""
|
||||||
self._state = STATE_IDLE
|
self._attr_activity = VacuumActivity.IDLE
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def return_to_base(self, **kwargs: Any) -> None:
|
def return_to_base(self, **kwargs: Any) -> None:
|
||||||
"""Return dock to charging base."""
|
"""Return dock to charging base."""
|
||||||
self._state = STATE_RETURNING
|
self._attr_activity = VacuumActivity.RETURNING
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
event.call_later(self.hass, 30, self.__set_state_to_dock)
|
event.call_later(self.hass, 30, self.__set_state_to_dock)
|
||||||
|
|
||||||
def clean_spot(self, **kwargs: Any) -> None:
|
def clean_spot(self, **kwargs: Any) -> None:
|
||||||
"""Perform a spot clean-up."""
|
"""Perform a spot clean-up."""
|
||||||
self._state = STATE_CLEANING
|
self._attr_activity = VacuumActivity.CLEANING
|
||||||
self._cleaned_area += 1.32
|
self._cleaned_area += 1.32
|
||||||
self._battery_level -= 1
|
self._battery_level -= 1
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@ -167,12 +158,12 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||||
"persistent_notification",
|
"persistent_notification",
|
||||||
service_data={"message": "I'm here!", "title": "Locate request"},
|
service_data={"message": "I'm here!", "title": "Locate request"},
|
||||||
)
|
)
|
||||||
self._state = STATE_IDLE
|
self._attr_activity = VacuumActivity.IDLE
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_clean_spot(self, **kwargs: Any) -> None:
|
async def async_clean_spot(self, **kwargs: Any) -> None:
|
||||||
"""Locate the vacuum's position."""
|
"""Locate the vacuum's position."""
|
||||||
self._state = STATE_CLEANING
|
self._attr_activity = VacuumActivity.CLEANING
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_send_command(
|
async def async_send_command(
|
||||||
|
@ -182,9 +173,9 @@ class StateDemoVacuum(StateVacuumEntity):
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Send a command to the vacuum."""
|
"""Send a command to the vacuum."""
|
||||||
self._state = STATE_IDLE
|
self._attr_activity = VacuumActivity.IDLE
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
def __set_state_to_dock(self, _: datetime) -> None:
|
def __set_state_to_dock(self, _: datetime) -> None:
|
||||||
self._state = STATE_DOCKED
|
self._attr_activity = VacuumActivity.DOCKED
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
@ -13,14 +13,9 @@ from deebot_client.models import CleanAction, CleanMode, Room, State
|
||||||
import sucks
|
import sucks
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
StateVacuumEntityDescription,
|
StateVacuumEntityDescription,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, SupportsResponse
|
from homeassistant.core import HomeAssistant, SupportsResponse
|
||||||
|
@ -123,22 +118,22 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def activity(self) -> VacuumActivity | None:
|
||||||
"""Return the state of the vacuum cleaner."""
|
"""Return the state of the vacuum cleaner."""
|
||||||
if self.error is not None:
|
if self.error is not None:
|
||||||
return STATE_ERROR
|
return VacuumActivity.ERROR
|
||||||
|
|
||||||
if self.device.is_cleaning:
|
if self.device.is_cleaning:
|
||||||
return STATE_CLEANING
|
return VacuumActivity.CLEANING
|
||||||
|
|
||||||
if self.device.is_charging:
|
if self.device.is_charging:
|
||||||
return STATE_DOCKED
|
return VacuumActivity.DOCKED
|
||||||
|
|
||||||
if self.device.vacuum_status == sucks.CLEAN_MODE_STOP:
|
if self.device.vacuum_status == sucks.CLEAN_MODE_STOP:
|
||||||
return STATE_IDLE
|
return VacuumActivity.IDLE
|
||||||
|
|
||||||
if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING:
|
if self.device.vacuum_status == sucks.CHARGE_MODE_RETURNING:
|
||||||
return STATE_RETURNING
|
return VacuumActivity.RETURNING
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -202,7 +197,7 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
|
||||||
|
|
||||||
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||||
"""Set fan speed."""
|
"""Set fan speed."""
|
||||||
if self.state == STATE_CLEANING:
|
if self.state == VacuumActivity.CLEANING:
|
||||||
self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed))
|
self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed))
|
||||||
|
|
||||||
def send_command(
|
def send_command(
|
||||||
|
@ -225,12 +220,12 @@ class EcovacsLegacyVacuum(EcovacsLegacyEntity, StateVacuumEntity):
|
||||||
|
|
||||||
|
|
||||||
_STATE_TO_VACUUM_STATE = {
|
_STATE_TO_VACUUM_STATE = {
|
||||||
State.IDLE: STATE_IDLE,
|
State.IDLE: VacuumActivity.IDLE,
|
||||||
State.CLEANING: STATE_CLEANING,
|
State.CLEANING: VacuumActivity.CLEANING,
|
||||||
State.RETURNING: STATE_RETURNING,
|
State.RETURNING: VacuumActivity.RETURNING,
|
||||||
State.DOCKED: STATE_DOCKED,
|
State.DOCKED: VacuumActivity.DOCKED,
|
||||||
State.ERROR: STATE_ERROR,
|
State.ERROR: VacuumActivity.ERROR,
|
||||||
State.PAUSED: STATE_PAUSED,
|
State.PAUSED: VacuumActivity.PAUSED,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ATTR_ROOMS = "rooms"
|
_ATTR_ROOMS = "rooms"
|
||||||
|
@ -284,7 +279,7 @@ class EcovacsVacuum(
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def on_status(event: StateEvent) -> None:
|
async def on_status(event: StateEvent) -> None:
|
||||||
self._attr_state = _STATE_TO_VACUUM_STATE[event.state]
|
self._attr_activity = _STATE_TO_VACUUM_STATE[event.state]
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
self._subscribe(self._capability.battery.event, on_battery)
|
self._subscribe(self._capability.battery.event, on_battery)
|
||||||
|
|
|
@ -21,8 +21,6 @@ from .models import Eq3Config, Eq3ConfigEntryData
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.NUMBER,
|
|
||||||
Platform.SWITCH,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
|
@ -21,14 +21,6 @@ DEVICE_MODEL = "CC-RT-BLE-EQ"
|
||||||
ENTITY_KEY_DST = "dst"
|
ENTITY_KEY_DST = "dst"
|
||||||
ENTITY_KEY_BATTERY = "battery"
|
ENTITY_KEY_BATTERY = "battery"
|
||||||
ENTITY_KEY_WINDOW = "window"
|
ENTITY_KEY_WINDOW = "window"
|
||||||
ENTITY_KEY_LOCK = "lock"
|
|
||||||
ENTITY_KEY_BOOST = "boost"
|
|
||||||
ENTITY_KEY_AWAY = "away"
|
|
||||||
ENTITY_KEY_COMFORT = "comfort"
|
|
||||||
ENTITY_KEY_ECO = "eco"
|
|
||||||
ENTITY_KEY_OFFSET = "offset"
|
|
||||||
ENTITY_KEY_WINDOW_OPEN_TEMPERATURE = "window_open_temperature"
|
|
||||||
ENTITY_KEY_WINDOW_OPEN_TIMEOUT = "window_open_timeout"
|
|
||||||
|
|
||||||
GET_DEVICE_TIMEOUT = 5 # seconds
|
GET_DEVICE_TIMEOUT = 5 # seconds
|
||||||
|
|
||||||
|
@ -82,5 +74,3 @@ DEFAULT_SCAN_INTERVAL = 10 # seconds
|
||||||
|
|
||||||
SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected"
|
SIGNAL_THERMOSTAT_DISCONNECTED = f"{DOMAIN}.thermostat_disconnected"
|
||||||
SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected"
|
SIGNAL_THERMOSTAT_CONNECTED = f"{DOMAIN}.thermostat_connected"
|
||||||
|
|
||||||
EQ3BT_STEP = 0.5
|
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"entity": {
|
|
||||||
"binary_sensor": {
|
|
||||||
"dst": {
|
|
||||||
"default": "mdi:sun-clock",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:sun-clock-outline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"comfort": {
|
|
||||||
"default": "mdi:sun-thermometer"
|
|
||||||
},
|
|
||||||
"eco": {
|
|
||||||
"default": "mdi:snowflake-thermometer"
|
|
||||||
},
|
|
||||||
"offset": {
|
|
||||||
"default": "mdi:thermometer-plus"
|
|
||||||
},
|
|
||||||
"window_open_temperature": {
|
|
||||||
"default": "mdi:window-open-variant"
|
|
||||||
},
|
|
||||||
"window_open_timeout": {
|
|
||||||
"default": "mdi:timer-refresh"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"switch": {
|
|
||||||
"away": {
|
|
||||||
"default": "mdi:home-account",
|
|
||||||
"state": {
|
|
||||||
"on": "mdi:home-export"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lock": {
|
|
||||||
"default": "mdi:lock",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:lock-off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"boost": {
|
|
||||||
"default": "mdi:fire",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:fire-off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,5 +23,5 @@
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["eq3btsmart"],
|
"loggers": ["eq3btsmart"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==1.1.0"]
|
"requirements": ["eq3btsmart==1.2.1", "bleak-esphome==1.1.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from eq3btsmart.const import DEFAULT_AWAY_HOURS, DEFAULT_AWAY_TEMP
|
||||||
from eq3btsmart.thermostat import Thermostat
|
from eq3btsmart.thermostat import Thermostat
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
@ -22,6 +23,8 @@ class Eq3Config:
|
||||||
target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR
|
target_temp_selector: TargetTemperatureSelector = DEFAULT_TARGET_TEMP_SELECTOR
|
||||||
external_temp_sensor: str = ""
|
external_temp_sensor: str = ""
|
||||||
scan_interval: int = DEFAULT_SCAN_INTERVAL
|
scan_interval: int = DEFAULT_SCAN_INTERVAL
|
||||||
|
default_away_hours: float = DEFAULT_AWAY_HOURS
|
||||||
|
default_away_temperature: float = DEFAULT_AWAY_TEMP
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
|
|
|
@ -1,158 +0,0 @@
|
||||||
"""Platform for eq3 number entities."""
|
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from eq3btsmart import Thermostat
|
|
||||||
from eq3btsmart.const import (
|
|
||||||
EQ3BT_MAX_OFFSET,
|
|
||||||
EQ3BT_MAX_TEMP,
|
|
||||||
EQ3BT_MIN_OFFSET,
|
|
||||||
EQ3BT_MIN_TEMP,
|
|
||||||
)
|
|
||||||
from eq3btsmart.models import Presets
|
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
|
||||||
NumberDeviceClass,
|
|
||||||
NumberEntity,
|
|
||||||
NumberEntityDescription,
|
|
||||||
NumberMode,
|
|
||||||
)
|
|
||||||
from homeassistant.const import EntityCategory, UnitOfTemperature, UnitOfTime
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from . import Eq3ConfigEntry
|
|
||||||
from .const import (
|
|
||||||
ENTITY_KEY_COMFORT,
|
|
||||||
ENTITY_KEY_ECO,
|
|
||||||
ENTITY_KEY_OFFSET,
|
|
||||||
ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
|
|
||||||
ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
|
|
||||||
EQ3BT_STEP,
|
|
||||||
)
|
|
||||||
from .entity import Eq3Entity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class Eq3NumberEntityDescription(NumberEntityDescription):
|
|
||||||
"""Entity description for eq3 number entities."""
|
|
||||||
|
|
||||||
value_func: Callable[[Presets], float]
|
|
||||||
value_set_func: Callable[
|
|
||||||
[Thermostat],
|
|
||||||
Callable[[float], Awaitable[None]],
|
|
||||||
]
|
|
||||||
mode: NumberMode = NumberMode.BOX
|
|
||||||
entity_category: EntityCategory | None = EntityCategory.CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
NUMBER_ENTITY_DESCRIPTIONS = [
|
|
||||||
Eq3NumberEntityDescription(
|
|
||||||
key=ENTITY_KEY_COMFORT,
|
|
||||||
value_func=lambda presets: presets.comfort_temperature.value,
|
|
||||||
value_set_func=lambda thermostat: thermostat.async_configure_comfort_temperature,
|
|
||||||
translation_key=ENTITY_KEY_COMFORT,
|
|
||||||
native_min_value=EQ3BT_MIN_TEMP,
|
|
||||||
native_max_value=EQ3BT_MAX_TEMP,
|
|
||||||
native_step=EQ3BT_STEP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
|
||||||
),
|
|
||||||
Eq3NumberEntityDescription(
|
|
||||||
key=ENTITY_KEY_ECO,
|
|
||||||
value_func=lambda presets: presets.eco_temperature.value,
|
|
||||||
value_set_func=lambda thermostat: thermostat.async_configure_eco_temperature,
|
|
||||||
translation_key=ENTITY_KEY_ECO,
|
|
||||||
native_min_value=EQ3BT_MIN_TEMP,
|
|
||||||
native_max_value=EQ3BT_MAX_TEMP,
|
|
||||||
native_step=EQ3BT_STEP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
|
||||||
),
|
|
||||||
Eq3NumberEntityDescription(
|
|
||||||
key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
|
|
||||||
value_func=lambda presets: presets.window_open_temperature.value,
|
|
||||||
value_set_func=lambda thermostat: thermostat.async_configure_window_open_temperature,
|
|
||||||
translation_key=ENTITY_KEY_WINDOW_OPEN_TEMPERATURE,
|
|
||||||
native_min_value=EQ3BT_MIN_TEMP,
|
|
||||||
native_max_value=EQ3BT_MAX_TEMP,
|
|
||||||
native_step=EQ3BT_STEP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
|
||||||
),
|
|
||||||
Eq3NumberEntityDescription(
|
|
||||||
key=ENTITY_KEY_OFFSET,
|
|
||||||
value_func=lambda presets: presets.offset_temperature.value,
|
|
||||||
value_set_func=lambda thermostat: thermostat.async_configure_temperature_offset,
|
|
||||||
translation_key=ENTITY_KEY_OFFSET,
|
|
||||||
native_min_value=EQ3BT_MIN_OFFSET,
|
|
||||||
native_max_value=EQ3BT_MAX_OFFSET,
|
|
||||||
native_step=EQ3BT_STEP,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
device_class=NumberDeviceClass.TEMPERATURE,
|
|
||||||
),
|
|
||||||
Eq3NumberEntityDescription(
|
|
||||||
key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
|
|
||||||
value_set_func=lambda thermostat: thermostat.async_configure_window_open_duration,
|
|
||||||
value_func=lambda presets: presets.window_open_time.value.total_seconds() / 60,
|
|
||||||
translation_key=ENTITY_KEY_WINDOW_OPEN_TIMEOUT,
|
|
||||||
native_min_value=0,
|
|
||||||
native_max_value=60,
|
|
||||||
native_step=5,
|
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: Eq3ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the entry."""
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
Eq3NumberEntity(entry, entity_description)
|
|
||||||
for entity_description in NUMBER_ENTITY_DESCRIPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Eq3NumberEntity(Eq3Entity, NumberEntity):
|
|
||||||
"""Base class for all eq3 number entities."""
|
|
||||||
|
|
||||||
entity_description: Eq3NumberEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, entry: Eq3ConfigEntry, entity_description: Eq3NumberEntityDescription
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
|
|
||||||
super().__init__(entry, entity_description.key)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float:
|
|
||||||
"""Return the state of the entity."""
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert self._thermostat.status is not None
|
|
||||||
assert self._thermostat.status.presets is not None
|
|
||||||
|
|
||||||
return self.entity_description.value_func(self._thermostat.status.presets)
|
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
|
||||||
"""Set the state of the entity."""
|
|
||||||
|
|
||||||
await self.entity_description.value_set_func(self._thermostat)(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return whether the entity is available."""
|
|
||||||
|
|
||||||
return (
|
|
||||||
self._thermostat.status is not None
|
|
||||||
and self._thermostat.status.presets is not None
|
|
||||||
and self._attr_available
|
|
||||||
)
|
|
|
@ -24,34 +24,6 @@
|
||||||
"dst": {
|
"dst": {
|
||||||
"name": "Daylight saving time"
|
"name": "Daylight saving time"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"comfort": {
|
|
||||||
"name": "Comfort temperature"
|
|
||||||
},
|
|
||||||
"eco": {
|
|
||||||
"name": "Eco temperature"
|
|
||||||
},
|
|
||||||
"offset": {
|
|
||||||
"name": "Offset temperature"
|
|
||||||
},
|
|
||||||
"window_open_temperature": {
|
|
||||||
"name": "Window open temperature"
|
|
||||||
},
|
|
||||||
"window_open_timeout": {
|
|
||||||
"name": "Window open timeout"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"switch": {
|
|
||||||
"lock": {
|
|
||||||
"name": "Lock"
|
|
||||||
},
|
|
||||||
"boost": {
|
|
||||||
"name": "Boost"
|
|
||||||
},
|
|
||||||
"away": {
|
|
||||||
"name": "Away"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
"""Platform for eq3 switch entities."""
|
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from eq3btsmart import Thermostat
|
|
||||||
from eq3btsmart.models import Status
|
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from . import Eq3ConfigEntry
|
|
||||||
from .const import ENTITY_KEY_AWAY, ENTITY_KEY_BOOST, ENTITY_KEY_LOCK
|
|
||||||
from .entity import Eq3Entity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class Eq3SwitchEntityDescription(SwitchEntityDescription):
|
|
||||||
"""Entity description for eq3 switch entities."""
|
|
||||||
|
|
||||||
toggle_func: Callable[[Thermostat], Callable[[bool], Awaitable[None]]]
|
|
||||||
value_func: Callable[[Status], bool]
|
|
||||||
|
|
||||||
|
|
||||||
SWITCH_ENTITY_DESCRIPTIONS = [
|
|
||||||
Eq3SwitchEntityDescription(
|
|
||||||
key=ENTITY_KEY_LOCK,
|
|
||||||
translation_key=ENTITY_KEY_LOCK,
|
|
||||||
toggle_func=lambda thermostat: thermostat.async_set_locked,
|
|
||||||
value_func=lambda status: status.is_locked,
|
|
||||||
),
|
|
||||||
Eq3SwitchEntityDescription(
|
|
||||||
key=ENTITY_KEY_BOOST,
|
|
||||||
translation_key=ENTITY_KEY_BOOST,
|
|
||||||
toggle_func=lambda thermostat: thermostat.async_set_boost,
|
|
||||||
value_func=lambda status: status.is_boost,
|
|
||||||
),
|
|
||||||
Eq3SwitchEntityDescription(
|
|
||||||
key=ENTITY_KEY_AWAY,
|
|
||||||
translation_key=ENTITY_KEY_AWAY,
|
|
||||||
toggle_func=lambda thermostat: thermostat.async_set_away,
|
|
||||||
value_func=lambda status: status.is_away,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: Eq3ConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the entry."""
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
Eq3SwitchEntity(entry, entity_description)
|
|
||||||
for entity_description in SWITCH_ENTITY_DESCRIPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Eq3SwitchEntity(Eq3Entity, SwitchEntity):
|
|
||||||
"""Base class for eq3 switch entities."""
|
|
||||||
|
|
||||||
entity_description: Eq3SwitchEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
entry: Eq3ConfigEntry,
|
|
||||||
entity_description: Eq3SwitchEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the entity."""
|
|
||||||
|
|
||||||
super().__init__(entry, entity_description.key)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn on the switch."""
|
|
||||||
|
|
||||||
await self.entity_description.toggle_func(self._thermostat)(True)
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn off the switch."""
|
|
||||||
|
|
||||||
await self.entity_description.toggle_func(self._thermostat)(False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return the state of the switch."""
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert self._thermostat.status is not None
|
|
||||||
|
|
||||||
return self.entity_description.value_func(self._thermostat.status)
|
|
|
@ -18,7 +18,7 @@
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"file_path": "The local file path to retrieve the sensor value from",
|
"file_path": "The local file path to retrieve the sensor value from",
|
||||||
"value_template": "A template to render the sensors value based on the file content",
|
"value_template": "A template to render the the sensors value based on the file content",
|
||||||
"unit_of_measurement": "Unit of measurement for the sensor"
|
"unit_of_measurement": "Unit of measurement for the sensor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -57,8 +57,6 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
_host: str
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
|
@ -69,6 +67,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize FRITZ!Box Tools flow."""
|
"""Initialize FRITZ!Box Tools flow."""
|
||||||
|
self._host: str | None = None
|
||||||
self._name: str = ""
|
self._name: str = ""
|
||||||
self._password: str = ""
|
self._password: str = ""
|
||||||
self._use_tls: bool = False
|
self._use_tls: bool = False
|
||||||
|
@ -113,6 +112,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
async def async_check_configured_entry(self) -> ConfigEntry | None:
|
async def async_check_configured_entry(self) -> ConfigEntry | None:
|
||||||
"""Check if entry is configured."""
|
"""Check if entry is configured."""
|
||||||
|
assert self._host
|
||||||
current_host = await self.hass.async_add_executor_job(
|
current_host = await self.hass.async_add_executor_job(
|
||||||
socket.gethostbyname, self._host
|
socket.gethostbyname, self._host
|
||||||
)
|
)
|
||||||
|
@ -154,17 +154,15 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle a flow initialized by discovery."""
|
"""Handle a flow initialized by discovery."""
|
||||||
ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "")
|
ssdp_location: ParseResult = urlparse(discovery_info.ssdp_location or "")
|
||||||
host = ssdp_location.hostname
|
self._host = ssdp_location.hostname
|
||||||
if not host or ipaddress.ip_address(host).is_link_local:
|
|
||||||
return self.async_abort(reason="ignore_ip6_link_local")
|
|
||||||
|
|
||||||
self._host = host
|
|
||||||
self._name = (
|
self._name = (
|
||||||
discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
|
discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
|
||||||
or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
|
or discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
|
||||||
)
|
)
|
||||||
|
|
||||||
uuid: str | None
|
if not self._host or ipaddress.ip_address(self._host).is_link_local:
|
||||||
|
return self.async_abort(reason="ignore_ip6_link_local")
|
||||||
|
|
||||||
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
|
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
|
||||||
if uuid.startswith("uuid:"):
|
if uuid.startswith("uuid:"):
|
||||||
uuid = uuid[5:]
|
uuid = uuid[5:]
|
||||||
|
|
|
@ -43,11 +43,10 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
_name: str
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize flow."""
|
"""Initialize flow."""
|
||||||
self._host: str | None = None
|
self._host: str | None = None
|
||||||
|
self._name: str | None = None
|
||||||
self._password: str | None = None
|
self._password: str | None = None
|
||||||
self._username: str | None = None
|
self._username: str | None = None
|
||||||
|
|
||||||
|
@ -159,6 +158,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
result = await self.async_try_connect()
|
result = await self.async_try_connect()
|
||||||
|
|
||||||
if result == RESULT_SUCCESS:
|
if result == RESULT_SUCCESS:
|
||||||
|
assert self._name is not None
|
||||||
return self._get_entry(self._name)
|
return self._get_entry(self._name)
|
||||||
if result != RESULT_INVALID_AUTH:
|
if result != RESULT_INVALID_AUTH:
|
||||||
return self.async_abort(reason=result)
|
return self.async_abort(reason=result)
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
"""The go2rtc component."""
|
"""The go2rtc component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
|
||||||
from awesomeversion import AwesomeVersion
|
|
||||||
from go2rtc_client import Go2RtcRestClient
|
from go2rtc_client import Go2RtcRestClient
|
||||||
from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
|
from go2rtc_client.exceptions import Go2RtcClientError, Go2RtcVersionError
|
||||||
from go2rtc_client.ws import (
|
from go2rtc_client.ws import (
|
||||||
|
@ -33,11 +35,7 @@ from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||||
config_validation as cv,
|
|
||||||
discovery_flow,
|
|
||||||
issue_registry as ir,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
@ -47,8 +45,8 @@ from .const import (
|
||||||
CONF_DEBUG_UI,
|
CONF_DEBUG_UI,
|
||||||
DEBUG_UI_URL_MESSAGE,
|
DEBUG_UI_URL_MESSAGE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
HA_MANAGED_RTSP_PORT,
|
||||||
HA_MANAGED_URL,
|
HA_MANAGED_URL,
|
||||||
RECOMMENDED_VERSION,
|
|
||||||
)
|
)
|
||||||
from .server import Server
|
from .server import Server
|
||||||
|
|
||||||
|
@ -96,13 +94,22 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
|
_DATA_GO2RTC: HassKey[Go2RtcData] = HassKey(DOMAIN)
|
||||||
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
|
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Go2RtcData:
|
||||||
|
"""Data for go2rtc."""
|
||||||
|
|
||||||
|
url: str
|
||||||
|
managed: bool
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up WebRTC."""
|
"""Set up WebRTC."""
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
|
managed = False
|
||||||
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
|
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
|
||||||
await _remove_go2rtc_entries(hass)
|
await _remove_go2rtc_entries(hass)
|
||||||
return True
|
return True
|
||||||
|
@ -137,8 +144,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
|
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
|
||||||
|
|
||||||
url = HA_MANAGED_URL
|
url = HA_MANAGED_URL
|
||||||
|
managed = True
|
||||||
|
|
||||||
hass.data[_DATA_GO2RTC] = url
|
hass.data[_DATA_GO2RTC] = Go2RtcData(url, managed)
|
||||||
discovery_flow.async_create_flow(
|
discovery_flow.async_create_flow(
|
||||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||||
)
|
)
|
||||||
|
@ -153,42 +161,32 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up go2rtc from a config entry."""
|
"""Set up go2rtc from a config entry."""
|
||||||
url = hass.data[_DATA_GO2RTC]
|
data = hass.data[_DATA_GO2RTC]
|
||||||
|
|
||||||
# Validate the server URL
|
# Validate the server URL
|
||||||
try:
|
try:
|
||||||
client = Go2RtcRestClient(async_get_clientsession(hass), url)
|
client = Go2RtcRestClient(async_get_clientsession(hass), data.url)
|
||||||
version = await client.validate_server_version()
|
await client.validate_server_version()
|
||||||
if version < AwesomeVersion(RECOMMENDED_VERSION):
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
"recommended_version",
|
|
||||||
is_fixable=False,
|
|
||||||
is_persistent=False,
|
|
||||||
severity=ir.IssueSeverity.WARNING,
|
|
||||||
translation_key="recommended_version",
|
|
||||||
translation_placeholders={
|
|
||||||
"recommended_version": RECOMMENDED_VERSION,
|
|
||||||
"current_version": str(version),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Go2RtcClientError as err:
|
except Go2RtcClientError as err:
|
||||||
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
|
if isinstance(err.__cause__, _RETRYABLE_ERRORS):
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
f"Could not connect to go2rtc instance on {url}"
|
f"Could not connect to go2rtc instance on {data.url}"
|
||||||
) from err
|
) from err
|
||||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
_LOGGER.warning(
|
||||||
|
"Could not connect to go2rtc instance on %s (%s)", data.url, err
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
except Go2RtcVersionError as err:
|
except Go2RtcVersionError as err:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
f"The go2rtc server version is not supported, {err}"
|
f"The go2rtc server version is not supported, {err}"
|
||||||
) from err
|
) from err
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
_LOGGER.warning("Could not connect to go2rtc instance on %s (%s)", url, err)
|
_LOGGER.warning(
|
||||||
|
"Could not connect to go2rtc instance on %s (%s)", data.url, err
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
provider = WebRTCProvider(hass, url)
|
provider = WebRTCProvider(hass, data)
|
||||||
async_register_webrtc_provider(hass, provider)
|
async_register_webrtc_provider(hass, provider)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -206,12 +204,12 @@ async def _get_binary(hass: HomeAssistant) -> str | None:
|
||||||
class WebRTCProvider(CameraWebRTCProvider):
|
class WebRTCProvider(CameraWebRTCProvider):
|
||||||
"""WebRTC provider."""
|
"""WebRTC provider."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, url: str) -> None:
|
def __init__(self, hass: HomeAssistant, data: Go2RtcData) -> None:
|
||||||
"""Initialize the WebRTC provider."""
|
"""Initialize the WebRTC provider."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._url = url
|
self._data = data
|
||||||
self._session = async_get_clientsession(hass)
|
self._session = async_get_clientsession(hass)
|
||||||
self._rest_client = Go2RtcRestClient(self._session, url)
|
self._rest_client = Go2RtcRestClient(self._session, data.url)
|
||||||
self._sessions: dict[str, Go2RtcWsClient] = {}
|
self._sessions: dict[str, Go2RtcWsClient] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -233,7 +231,7 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle the WebRTC offer and return the answer via the provided callback."""
|
"""Handle the WebRTC offer and return the answer via the provided callback."""
|
||||||
self._sessions[session_id] = ws_client = Go2RtcWsClient(
|
self._sessions[session_id] = ws_client = Go2RtcWsClient(
|
||||||
self._session, self._url, source=camera.entity_id
|
self._session, self._data.url, source=camera.entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (stream_source := await camera.stream_source()):
|
if not (stream_source := await camera.stream_source()):
|
||||||
|
@ -244,18 +242,34 @@ class WebRTCProvider(CameraWebRTCProvider):
|
||||||
|
|
||||||
streams = await self._rest_client.streams.list()
|
streams = await self._rest_client.streams.list()
|
||||||
|
|
||||||
if (stream := streams.get(camera.entity_id)) is None or not any(
|
if self._data.managed:
|
||||||
stream_source == producer.url for producer in stream.producers
|
# HA manages the go2rtc instance
|
||||||
|
stream_original_name = f"{camera.entity_id}_original"
|
||||||
|
stream_redirect_sources = [
|
||||||
|
f"rtsp://127.0.0.1:{HA_MANAGED_RTSP_PORT}/{stream_original_name}",
|
||||||
|
f"ffmpeg:{stream_original_name}#audio=opus",
|
||||||
|
]
|
||||||
|
|
||||||
|
if (
|
||||||
|
(stream_org := streams.get(stream_original_name)) is None
|
||||||
|
or not any(
|
||||||
|
stream_source == producer.url for producer in stream_org.producers
|
||||||
|
)
|
||||||
|
or (stream_redirect := streams.get(camera.entity_id)) is None
|
||||||
|
or stream_redirect_sources != [p.url for p in stream_redirect.producers]
|
||||||
|
):
|
||||||
|
await self._rest_client.streams.add(stream_original_name, stream_source)
|
||||||
|
await self._rest_client.streams.add(
|
||||||
|
camera.entity_id, stream_redirect_sources
|
||||||
|
)
|
||||||
|
|
||||||
|
# go2rtc instance is managed outside HA
|
||||||
|
elif (stream_org := streams.get(camera.entity_id)) is None or not any(
|
||||||
|
stream_source == producer.url for producer in stream_org.producers
|
||||||
):
|
):
|
||||||
await self._rest_client.streams.add(
|
await self._rest_client.streams.add(
|
||||||
camera.entity_id,
|
camera.entity_id,
|
||||||
[
|
[stream_source, f"ffmpeg:{camera.entity_id}#audio=opus"],
|
||||||
stream_source,
|
|
||||||
# We are setting any ffmpeg rtsp related logs to debug
|
|
||||||
# Connection problems to the camera will be logged by the first stream
|
|
||||||
# Therefore setting it to debug will not hide any important logs
|
|
||||||
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -6,4 +6,4 @@ CONF_DEBUG_UI = "debug_ui"
|
||||||
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
|
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
|
||||||
HA_MANAGED_API_PORT = 11984
|
HA_MANAGED_API_PORT = 11984
|
||||||
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
|
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
|
||||||
RECOMMENDED_VERSION = "1.9.7"
|
HA_MANAGED_RTSP_PORT = 18554
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/go2rtc",
|
"documentation": "https://www.home-assistant.io/integrations/go2rtc",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["go2rtc-client==0.1.1"],
|
"requirements": ["go2rtc-client==0.1.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
|
from .const import HA_MANAGED_API_PORT, HA_MANAGED_RTSP_PORT, HA_MANAGED_URL
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_TERMINATE_TIMEOUT = 5
|
_TERMINATE_TIMEOUT = 5
|
||||||
|
@ -33,7 +33,7 @@ api:
|
||||||
listen: "{api_ip}:{api_port}"
|
listen: "{api_ip}:{api_port}"
|
||||||
|
|
||||||
rtsp:
|
rtsp:
|
||||||
listen: "127.0.0.1:18554"
|
listen: "127.0.0.1:{rtsp_port}"
|
||||||
|
|
||||||
webrtc:
|
webrtc:
|
||||||
listen: ":18555/tcp"
|
listen: ":18555/tcp"
|
||||||
|
@ -68,7 +68,9 @@ def _create_temp_file(api_ip: str) -> str:
|
||||||
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
|
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
|
||||||
file.write(
|
file.write(
|
||||||
_GO2RTC_CONFIG_FORMAT.format(
|
_GO2RTC_CONFIG_FORMAT.format(
|
||||||
api_ip=api_ip, api_port=HA_MANAGED_API_PORT
|
api_ip=api_ip,
|
||||||
|
api_port=HA_MANAGED_API_PORT,
|
||||||
|
rtsp_port=HA_MANAGED_RTSP_PORT,
|
||||||
).encode()
|
).encode()
|
||||||
)
|
)
|
||||||
return file.name
|
return file.name
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"issues": {
|
|
||||||
"recommended_version": {
|
|
||||||
"title": "Outdated go2rtc server detected",
|
|
||||||
"description": "We detected that you are using an outdated go2rtc server version. For the best experience, we recommend updating the go2rtc server to version `{recommended_version}`.\nCurrently you are using version `{current_version}`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -729,7 +729,7 @@ class DockTrait(_Trait):
|
||||||
|
|
||||||
def query_attributes(self) -> dict[str, Any]:
|
def query_attributes(self) -> dict[str, Any]:
|
||||||
"""Return dock query attributes."""
|
"""Return dock query attributes."""
|
||||||
return {"isDocked": self.state.state == vacuum.STATE_DOCKED}
|
return {"isDocked": self.state.state == vacuum.VacuumActivity.DOCKED}
|
||||||
|
|
||||||
async def execute(self, command, data, params, challenge):
|
async def execute(self, command, data, params, challenge):
|
||||||
"""Execute a dock command."""
|
"""Execute a dock command."""
|
||||||
|
@ -825,8 +825,8 @@ class EnergyStorageTrait(_Trait):
|
||||||
"capacityUntilFull": [
|
"capacityUntilFull": [
|
||||||
{"rawValue": 100 - battery_level, "unit": "PERCENTAGE"}
|
{"rawValue": 100 - battery_level, "unit": "PERCENTAGE"}
|
||||||
],
|
],
|
||||||
"isCharging": self.state.state == vacuum.STATE_DOCKED,
|
"isCharging": self.state.state == vacuum.VacuumActivity.DOCKED,
|
||||||
"isPluggedIn": self.state.state == vacuum.STATE_DOCKED,
|
"isPluggedIn": self.state.state == vacuum.VacuumActivity.DOCKED,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, command, data, params, challenge):
|
async def execute(self, command, data, params, challenge):
|
||||||
|
@ -882,8 +882,8 @@ class StartStopTrait(_Trait):
|
||||||
|
|
||||||
if domain == vacuum.DOMAIN:
|
if domain == vacuum.DOMAIN:
|
||||||
return {
|
return {
|
||||||
"isRunning": state == vacuum.STATE_CLEANING,
|
"isRunning": state == vacuum.VacuumActivity.CLEANING,
|
||||||
"isPaused": state == vacuum.STATE_PAUSED,
|
"isPaused": state == vacuum.VacuumActivity.PAUSED,
|
||||||
}
|
}
|
||||||
|
|
||||||
if domain in COVER_VALVE_DOMAINS:
|
if domain in COVER_VALVE_DOMAINS:
|
||||||
|
|
|
@ -11,7 +11,7 @@ from typing import Protocol
|
||||||
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
|
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
|
||||||
from homeassistant.components.climate import HVACMode
|
from homeassistant.components.climate import HVACMode
|
||||||
from homeassistant.components.lock import LockState
|
from homeassistant.components.lock import LockState
|
||||||
from homeassistant.components.vacuum import STATE_CLEANING, STATE_ERROR, STATE_RETURNING
|
from homeassistant.components.vacuum import VacuumActivity
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (
|
||||||
STATE_ECO,
|
STATE_ECO,
|
||||||
STATE_ELECTRIC,
|
STATE_ELECTRIC,
|
||||||
|
@ -105,9 +105,9 @@ ON_OFF_STATES: dict[Platform | str, tuple[set[str], str, str]] = {
|
||||||
Platform.VACUUM: (
|
Platform.VACUUM: (
|
||||||
{
|
{
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_CLEANING,
|
VacuumActivity.CLEANING,
|
||||||
STATE_RETURNING,
|
VacuumActivity.RETURNING,
|
||||||
STATE_ERROR,
|
VacuumActivity.ERROR,
|
||||||
},
|
},
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.components.vacuum import (
|
||||||
DOMAIN as VACUUM_DOMAIN,
|
DOMAIN as VACUUM_DOMAIN,
|
||||||
SERVICE_RETURN_TO_BASE,
|
SERVICE_RETURN_TO_BASE,
|
||||||
SERVICE_START,
|
SERVICE_START,
|
||||||
STATE_CLEANING,
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -213,7 +213,7 @@ class Vacuum(Switch):
|
||||||
@callback
|
@callback
|
||||||
def async_update_state(self, new_state: State) -> None:
|
def async_update_state(self, new_state: State) -> None:
|
||||||
"""Update switch state after state changed."""
|
"""Update switch state after state changed."""
|
||||||
current_state = new_state.state in (STATE_CLEANING, STATE_ON)
|
current_state = new_state.state in (VacuumActivity.CLEANING, STATE_ON)
|
||||||
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
|
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
|
||||||
self.char_on.set_value(current_state)
|
self.char_on.set_value(current_state)
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ class PowerViewNumber(ShadeEntity, RestoreNumber):
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
def set_native_value(self, value: float) -> None:
|
||||||
"""Update the current value."""
|
"""Update the current value."""
|
||||||
self._attr_native_value = value
|
self._attr_native_value = value
|
||||||
self.entity_description.store_value_fn(self.coordinator, self._shade.id, value)
|
self.entity_description.store_value_fn(self.coordinator, self._shade.id, value)
|
||||||
|
|
|
@ -8,7 +8,6 @@ from aioautomower.exceptions import (
|
||||||
ApiException,
|
ApiException,
|
||||||
AuthException,
|
AuthException,
|
||||||
HusqvarnaWSServerHandshakeError,
|
HusqvarnaWSServerHandshakeError,
|
||||||
TimeoutException,
|
|
||||||
)
|
)
|
||||||
from aioautomower.model import MowerAttributes
|
from aioautomower.model import MowerAttributes
|
||||||
from aioautomower.session import AutomowerSession
|
from aioautomower.session import AutomowerSession
|
||||||
|
@ -23,7 +22,6 @@ from .const import DOMAIN
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
MAX_WS_RECONNECT_TIME = 600
|
MAX_WS_RECONNECT_TIME = 600
|
||||||
SCAN_INTERVAL = timedelta(minutes=8)
|
SCAN_INTERVAL = timedelta(minutes=8)
|
||||||
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
|
|
||||||
|
|
||||||
|
|
||||||
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
|
class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttributes]]):
|
||||||
|
@ -42,8 +40,8 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
self.api = api
|
self.api = api
|
||||||
|
|
||||||
self.ws_connected: bool = False
|
self.ws_connected: bool = False
|
||||||
self.reconnect_time = DEFAULT_RECONNECT_TIME
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, MowerAttributes]:
|
async def _async_update_data(self) -> dict[str, MowerAttributes]:
|
||||||
"""Subscribe for websocket and poll data from the API."""
|
"""Subscribe for websocket and poll data from the API."""
|
||||||
|
@ -68,28 +66,24 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[dict[str, MowerAttrib
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
automower_client: AutomowerSession,
|
automower_client: AutomowerSession,
|
||||||
|
reconnect_time: int = 2,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Listen with the client."""
|
"""Listen with the client."""
|
||||||
try:
|
try:
|
||||||
await automower_client.auth.websocket_connect()
|
await automower_client.auth.websocket_connect()
|
||||||
# Reset reconnect time after successful connection
|
reconnect_time = 2
|
||||||
self.reconnect_time = DEFAULT_RECONNECT_TIME
|
|
||||||
await automower_client.start_listening()
|
await automower_client.start_listening()
|
||||||
except HusqvarnaWSServerHandshakeError as err:
|
except HusqvarnaWSServerHandshakeError as err:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Failed to connect to websocket. Trying to reconnect: %s",
|
"Failed to connect to websocket. Trying to reconnect: %s", err
|
||||||
err,
|
|
||||||
)
|
|
||||||
except TimeoutException as err:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Failed to listen to websocket. Trying to reconnect: %s",
|
|
||||||
err,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not hass.is_stopping:
|
if not hass.is_stopping:
|
||||||
await asyncio.sleep(self.reconnect_time)
|
await asyncio.sleep(reconnect_time)
|
||||||
self.reconnect_time = min(self.reconnect_time * 2, MAX_WS_RECONNECT_TIME)
|
reconnect_time = min(reconnect_time * 2, MAX_WS_RECONNECT_TIME)
|
||||||
entry.async_create_background_task(
|
await self.client_listen(
|
||||||
hass,
|
hass=hass,
|
||||||
self.client_listen(hass, entry, automower_client),
|
entry=entry,
|
||||||
"reconnect_task",
|
automower_client=automower_client,
|
||||||
|
reconnect_time=reconnect_time,
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,23 +3,30 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from huum.exceptions import Forbidden, NotAuthenticated
|
|
||||||
from huum.huum import Huum
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN, PLATFORMS
|
from .const import DOMAIN, PLATFORMS
|
||||||
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
|
from huum.exceptions import Forbidden, NotAuthenticated
|
||||||
|
from huum.huum import Huum
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Huum from a config entry."""
|
"""Set up Huum from a config entry."""
|
||||||
|
if sys.version_info >= (3, 13):
|
||||||
|
raise HomeAssistantError(
|
||||||
|
"Huum is not supported on Python 3.13. Please use Python 3.12."
|
||||||
|
)
|
||||||
|
|
||||||
username = entry.data[CONF_USERNAME]
|
username = entry.data[CONF_USERNAME]
|
||||||
password = entry.data[CONF_PASSWORD]
|
password = entry.data[CONF_PASSWORD]
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from huum.const import SaunaStatus
|
|
||||||
from huum.exceptions import SafetyException
|
|
||||||
from huum.huum import Huum
|
|
||||||
from huum.schemas import HuumStatusResponse
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
|
@ -24,6 +20,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
|
from huum.const import SaunaStatus
|
||||||
|
from huum.exceptions import SafetyException
|
||||||
|
from huum.huum import Huum
|
||||||
|
from huum.schemas import HuumStatusResponse
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from huum.exceptions import Forbidden, NotAuthenticated
|
|
||||||
from huum.huum import Huum
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
@ -15,6 +14,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
if sys.version_info < (3, 13):
|
||||||
|
from huum.exceptions import Forbidden, NotAuthenticated
|
||||||
|
from huum.huum import Huum
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/huum",
|
"documentation": "https://www.home-assistant.io/integrations/huum",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["huum==0.7.12"]
|
"requirements": ["huum==0.7.11;python_version<'3.13'"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,7 @@ from homeassistant.const import (
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.typing import ConfigType
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ADD_ENTITIES_CALLBACKS,
|
ADD_ENTITIES_CALLBACKS,
|
||||||
|
@ -42,26 +41,15 @@ from .helpers import (
|
||||||
register_lcn_address_devices,
|
register_lcn_address_devices,
|
||||||
register_lcn_host_device,
|
register_lcn_host_device,
|
||||||
)
|
)
|
||||||
from .services import register_services
|
from .services import SERVICES
|
||||||
from .websocket import register_panel_and_ws_api
|
from .websocket import register_panel_and_ws_api
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
||||||
"""Set up the LCN component."""
|
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
await register_services(hass)
|
|
||||||
await register_panel_and_ws_api(hass)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
"""Set up a connection to PCHK host from a config entry."""
|
"""Set up a connection to PCHK host from a config entry."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
if config_entry.entry_id in hass.data[DOMAIN]:
|
if config_entry.entry_id in hass.data[DOMAIN]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -121,6 +109,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||||
)
|
)
|
||||||
lcn_connection.register_for_inputs(input_received)
|
lcn_connection.register_for_inputs(input_received)
|
||||||
|
|
||||||
|
# register service calls
|
||||||
|
for service_name, service in SERVICES:
|
||||||
|
if not hass.services.has_service(DOMAIN, service_name):
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, service_name, service(hass).async_call_service, service.schema
|
||||||
|
)
|
||||||
|
|
||||||
|
await register_panel_and_ws_api(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,6 +168,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||||
host = hass.data[DOMAIN].pop(config_entry.entry_id)
|
host = hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||||
await host[CONNECTION].async_close()
|
await host[CONNECTION].async_close()
|
||||||
|
|
||||||
|
# unregister service calls
|
||||||
|
if unload_ok and not hass.data[DOMAIN]: # check if this is the last entry to unload
|
||||||
|
for service_name, _ in SERVICES:
|
||||||
|
hass.services.async_remove(DOMAIN, service_name)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -429,11 +429,3 @@ SERVICES = (
|
||||||
(LcnService.DYN_TEXT, DynText),
|
(LcnService.DYN_TEXT, DynText),
|
||||||
(LcnService.PCK, Pck),
|
(LcnService.PCK, Pck),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def register_services(hass: HomeAssistant) -> None:
|
|
||||||
"""Register services for LCN."""
|
|
||||||
for service_name, service in SERVICES:
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN, service_name, service(hass).async_call_service, service.schema
|
|
||||||
)
|
|
||||||
|
|
|
@ -72,11 +72,8 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
|
||||||
super().__init__(coordinator, entity_description, property_id)
|
super().__init__(coordinator, entity_description, property_id)
|
||||||
|
|
||||||
self._ordered_named_fan_speeds = []
|
self._ordered_named_fan_speeds = []
|
||||||
self._attr_supported_features = (
|
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||||
FanEntityFeature.SET_SPEED
|
|
||||||
| FanEntityFeature.TURN_ON
|
|
||||||
| FanEntityFeature.TURN_OFF
|
|
||||||
)
|
|
||||||
if (fan_modes := self.data.fan_modes) is not None:
|
if (fan_modes := self.data.fan_modes) is not None:
|
||||||
self._attr_speed_count = len(fan_modes)
|
self._attr_speed_count = len(fan_modes)
|
||||||
if self.speed_count == 4:
|
if self.speed_count == 4:
|
||||||
|
@ -101,7 +98,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
|
||||||
self._attr_percentage = 0
|
self._attr_percentage = 0
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"[%s:%s] update status: %s -> %s (percentage=%s)",
|
"[%s:%s] update status: %s -> %s (percntage=%s)",
|
||||||
self.coordinator.device_name,
|
self.coordinator.device_name,
|
||||||
self.property_id,
|
self.property_id,
|
||||||
self.data.is_on,
|
self.data.is_on,
|
||||||
|
@ -123,7 +120,7 @@ class ThinQFanEntity(ThinQEntity, FanEntity):
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"[%s:%s] async_set_percentage. percentage=%s, value=%s",
|
"[%s:%s] async_set_percentage. percntage=%s, value=%s",
|
||||||
self.coordinator.device_name,
|
self.coordinator.device_name,
|
||||||
self.property_id,
|
self.property_id,
|
||||||
percentage,
|
percentage,
|
||||||
|
|
|
@ -9,15 +9,11 @@ from thinqconnect import DeviceType
|
||||||
from thinqconnect.integration import ExtendedProperty
|
from thinqconnect.integration import ExtendedProperty
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
StateVacuumEntityDescription,
|
StateVacuumEntityDescription,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_IDLE, STATE_PAUSED
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
@ -46,21 +42,21 @@ class State(StrEnum):
|
||||||
|
|
||||||
|
|
||||||
ROBOT_STATUS_TO_HA = {
|
ROBOT_STATUS_TO_HA = {
|
||||||
"charging": STATE_DOCKED,
|
"charging": VacuumActivity.DOCKED,
|
||||||
"diagnosis": STATE_IDLE,
|
"diagnosis": VacuumActivity.IDLE,
|
||||||
"homing": STATE_RETURNING,
|
"homing": VacuumActivity.RETURNING,
|
||||||
"initializing": STATE_IDLE,
|
"initializing": VacuumActivity.IDLE,
|
||||||
"macrosector": STATE_IDLE,
|
"macrosector": VacuumActivity.IDLE,
|
||||||
"monitoring_detecting": STATE_IDLE,
|
"monitoring_detecting": VacuumActivity.IDLE,
|
||||||
"monitoring_moving": STATE_IDLE,
|
"monitoring_moving": VacuumActivity.IDLE,
|
||||||
"monitoring_positioning": STATE_IDLE,
|
"monitoring_positioning": VacuumActivity.IDLE,
|
||||||
"pause": STATE_PAUSED,
|
"pause": VacuumActivity.PAUSED,
|
||||||
"reservation": STATE_IDLE,
|
"reservation": VacuumActivity.IDLE,
|
||||||
"setdate": STATE_IDLE,
|
"setdate": VacuumActivity.IDLE,
|
||||||
"sleep": STATE_IDLE,
|
"sleep": VacuumActivity.IDLE,
|
||||||
"standby": STATE_IDLE,
|
"standby": VacuumActivity.IDLE,
|
||||||
"working": STATE_CLEANING,
|
"working": VacuumActivity.CLEANING,
|
||||||
"error": STATE_ERROR,
|
"error": VacuumActivity.ERROR,
|
||||||
}
|
}
|
||||||
ROBOT_BATT_TO_HA = {
|
ROBOT_BATT_TO_HA = {
|
||||||
"moveless": 5,
|
"moveless": 5,
|
||||||
|
@ -114,7 +110,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
|
||||||
super()._update_status()
|
super()._update_status()
|
||||||
|
|
||||||
# Update state.
|
# Update state.
|
||||||
self._attr_state = ROBOT_STATUS_TO_HA[self.data.current_state]
|
self._attr_activity = ROBOT_STATUS_TO_HA[self.data.current_state]
|
||||||
|
|
||||||
# Update battery.
|
# Update battery.
|
||||||
if (level := self.data.battery) is not None:
|
if (level := self.data.battery) is not None:
|
||||||
|
@ -135,7 +131,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
|
||||||
"""Start the device."""
|
"""Start the device."""
|
||||||
if self.data.current_state == State.SLEEP:
|
if self.data.current_state == State.SLEEP:
|
||||||
value = State.WAKE_UP
|
value = State.WAKE_UP
|
||||||
elif self._attr_state == STATE_PAUSED:
|
elif self._attr_activity == VacuumActivity.PAUSED:
|
||||||
value = State.RESUME
|
value = State.RESUME
|
||||||
else:
|
else:
|
||||||
value = State.START
|
value = State.START
|
||||||
|
|
|
@ -10,12 +10,9 @@ from pylitterbot.enums import LitterBoxStatus
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_PAUSED,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
StateVacuumEntityDescription,
|
StateVacuumEntityDescription,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -29,16 +26,16 @@ from .entity import LitterRobotEntity
|
||||||
SERVICE_SET_SLEEP_MODE = "set_sleep_mode"
|
SERVICE_SET_SLEEP_MODE = "set_sleep_mode"
|
||||||
|
|
||||||
LITTER_BOX_STATUS_STATE_MAP = {
|
LITTER_BOX_STATUS_STATE_MAP = {
|
||||||
LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING,
|
LitterBoxStatus.CLEAN_CYCLE: VacuumActivity.CLEANING,
|
||||||
LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING,
|
LitterBoxStatus.EMPTY_CYCLE: VacuumActivity.CLEANING,
|
||||||
LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED,
|
LitterBoxStatus.CLEAN_CYCLE_COMPLETE: VacuumActivity.DOCKED,
|
||||||
LitterBoxStatus.CAT_DETECTED: STATE_DOCKED,
|
LitterBoxStatus.CAT_DETECTED: VacuumActivity.DOCKED,
|
||||||
LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED,
|
LitterBoxStatus.CAT_SENSOR_TIMING: VacuumActivity.DOCKED,
|
||||||
LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED,
|
LitterBoxStatus.DRAWER_FULL_1: VacuumActivity.DOCKED,
|
||||||
LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED,
|
LitterBoxStatus.DRAWER_FULL_2: VacuumActivity.DOCKED,
|
||||||
LitterBoxStatus.READY: STATE_DOCKED,
|
LitterBoxStatus.READY: VacuumActivity.DOCKED,
|
||||||
LitterBoxStatus.CAT_SENSOR_INTERRUPTED: STATE_PAUSED,
|
LitterBoxStatus.CAT_SENSOR_INTERRUPTED: VacuumActivity.PAUSED,
|
||||||
LitterBoxStatus.OFF: STATE_DOCKED,
|
LitterBoxStatus.OFF: VacuumActivity.DOCKED,
|
||||||
}
|
}
|
||||||
|
|
||||||
LITTER_BOX_ENTITY = StateVacuumEntityDescription(
|
LITTER_BOX_ENTITY = StateVacuumEntityDescription(
|
||||||
|
@ -78,9 +75,9 @@ class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def activity(self) -> VacuumActivity:
|
||||||
"""Return the state of the cleaner."""
|
"""Return the state of the cleaner."""
|
||||||
return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, STATE_ERROR)
|
return LITTER_BOX_STATUS_STATE_MAP.get(self.robot.status, VacuumActivity.ERROR)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def status(self) -> str:
|
def status(self) -> str:
|
||||||
|
|
|
@ -9,16 +9,13 @@ from chip.clusters import Objects as clusters
|
||||||
from matter_server.client.models import device_types
|
from matter_server.client.models import device_types
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
StateVacuumEntityDescription,
|
StateVacuumEntityDescription,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import STATE_IDLE, Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
@ -127,25 +124,25 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||||
operational_state: int = self.get_matter_attribute_value(
|
operational_state: int = self.get_matter_attribute_value(
|
||||||
clusters.RvcOperationalState.Attributes.OperationalState
|
clusters.RvcOperationalState.Attributes.OperationalState
|
||||||
)
|
)
|
||||||
state: str | None = None
|
state: VacuumActivity | None = None
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert self._supported_run_modes is not None
|
assert self._supported_run_modes is not None
|
||||||
if operational_state in (OperationalState.CHARGING, OperationalState.DOCKED):
|
if operational_state in (OperationalState.CHARGING, OperationalState.DOCKED):
|
||||||
state = STATE_DOCKED
|
state = VacuumActivity.DOCKED
|
||||||
elif operational_state == OperationalState.SEEKING_CHARGER:
|
elif operational_state == OperationalState.SEEKING_CHARGER:
|
||||||
state = STATE_RETURNING
|
state = VacuumActivity.RETURNING
|
||||||
elif operational_state in (
|
elif operational_state in (
|
||||||
OperationalState.UNABLE_TO_COMPLETE_OPERATION,
|
OperationalState.UNABLE_TO_COMPLETE_OPERATION,
|
||||||
OperationalState.UNABLE_TO_START_OR_RESUME,
|
OperationalState.UNABLE_TO_START_OR_RESUME,
|
||||||
):
|
):
|
||||||
state = STATE_ERROR
|
state = VacuumActivity.ERROR
|
||||||
elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
|
elif (run_mode := self._supported_run_modes.get(run_mode_raw)) is not None:
|
||||||
tags = {x.value for x in run_mode.modeTags}
|
tags = {x.value for x in run_mode.modeTags}
|
||||||
if ModeTag.CLEANING in tags:
|
if ModeTag.CLEANING in tags:
|
||||||
state = STATE_CLEANING
|
state = VacuumActivity.CLEANING
|
||||||
elif ModeTag.IDLE in tags:
|
elif ModeTag.IDLE in tags:
|
||||||
state = STATE_IDLE
|
state = VacuumActivity.IDLE
|
||||||
self._attr_state = state
|
self._attr_activity = state
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _calculate_features(self) -> None:
|
def _calculate_features(self) -> None:
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["mill", "mill_local"],
|
"loggers": ["mill", "mill_local"],
|
||||||
"requirements": ["millheater==0.12.2", "mill-local==0.3.0"]
|
"requirements": ["millheater==0.11.8", "mill-local==0.3.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,11 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import zeroconf
|
from homeassistant.components import zeroconf
|
||||||
from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
|
||||||
|
|
||||||
|
|
||||||
class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
|
class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a ModernForms config flow."""
|
"""Handle a ModernForms config flow."""
|
||||||
|
@ -57,21 +55,17 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
self, user_input: dict[str, Any] | None = None, prepare: bool = False
|
self, user_input: dict[str, Any] | None = None, prepare: bool = False
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Config flow handler for ModernForms."""
|
"""Config flow handler for ModernForms."""
|
||||||
|
source = self.context["source"]
|
||||||
|
|
||||||
# Request user input, unless we are preparing discovery flow
|
# Request user input, unless we are preparing discovery flow
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
user_input = {}
|
user_input = {}
|
||||||
if not prepare:
|
if not prepare:
|
||||||
if self.source == SOURCE_ZEROCONF:
|
if source == SOURCE_ZEROCONF:
|
||||||
return self.async_show_form(
|
return self._show_confirm_dialog()
|
||||||
step_id="zeroconf_confirm",
|
return self._show_setup_form()
|
||||||
description_placeholders={"name": self.name},
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=USER_SCHEMA,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.source == SOURCE_ZEROCONF:
|
if source == SOURCE_ZEROCONF:
|
||||||
user_input[CONF_HOST] = self.host
|
user_input[CONF_HOST] = self.host
|
||||||
user_input[CONF_MAC] = self.mac
|
user_input[CONF_MAC] = self.mac
|
||||||
|
|
||||||
|
@ -81,21 +75,18 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
try:
|
try:
|
||||||
device = await device.update()
|
device = await device.update()
|
||||||
except ModernFormsConnectionError:
|
except ModernFormsConnectionError:
|
||||||
if self.source == SOURCE_ZEROCONF:
|
if source == SOURCE_ZEROCONF:
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
return self.async_show_form(
|
return self._show_setup_form({"base": "cannot_connect"})
|
||||||
step_id="user",
|
|
||||||
data_schema=USER_SCHEMA,
|
|
||||||
errors={"base": "cannot_connect"},
|
|
||||||
)
|
|
||||||
user_input[CONF_MAC] = device.info.mac_address
|
user_input[CONF_MAC] = device.info.mac_address
|
||||||
|
user_input[CONF_NAME] = device.info.device_name
|
||||||
|
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
await self.async_set_unique_id(user_input[CONF_MAC])
|
await self.async_set_unique_id(user_input[CONF_MAC])
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
|
||||||
|
|
||||||
title = device.info.device_name
|
title = device.info.device_name
|
||||||
if self.source == SOURCE_ZEROCONF:
|
if source == SOURCE_ZEROCONF:
|
||||||
title = self.name
|
title = self.name
|
||||||
|
|
||||||
if prepare:
|
if prepare:
|
||||||
|
@ -105,3 +96,19 @@ class ModernFormsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
title=title,
|
title=title,
|
||||||
data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]},
|
data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _show_setup_form(self, errors: dict | None = None) -> ConfigFlowResult:
|
||||||
|
"""Show the setup form to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_confirm_dialog(self, errors: dict | None = None) -> ConfigFlowResult:
|
||||||
|
"""Show the confirm dialog to the user."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="zeroconf_confirm",
|
||||||
|
description_placeholders={"name": self.name},
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
||||||
|
|
|
@ -10,20 +10,12 @@ import voluptuous as vol
|
||||||
from homeassistant.components import vacuum
|
from homeassistant.components import vacuum
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
|
||||||
ATTR_SUPPORTED_FEATURES,
|
|
||||||
CONF_NAME,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
@ -43,13 +35,20 @@ BATTERY = "battery_level"
|
||||||
FAN_SPEED = "fan_speed"
|
FAN_SPEED = "fan_speed"
|
||||||
STATE = "state"
|
STATE = "state"
|
||||||
|
|
||||||
POSSIBLE_STATES: dict[str, str] = {
|
STATE_IDLE = "idle"
|
||||||
STATE_IDLE: STATE_IDLE,
|
STATE_DOCKED = "docked"
|
||||||
STATE_DOCKED: STATE_DOCKED,
|
STATE_ERROR = "error"
|
||||||
STATE_ERROR: STATE_ERROR,
|
STATE_PAUSED = "paused"
|
||||||
STATE_PAUSED: STATE_PAUSED,
|
STATE_RETURNING = "returning"
|
||||||
STATE_RETURNING: STATE_RETURNING,
|
STATE_CLEANING = "cleaning"
|
||||||
STATE_CLEANING: STATE_CLEANING,
|
|
||||||
|
POSSIBLE_STATES: dict[str, VacuumActivity] = {
|
||||||
|
STATE_IDLE: VacuumActivity.IDLE,
|
||||||
|
STATE_DOCKED: VacuumActivity.DOCKED,
|
||||||
|
STATE_ERROR: VacuumActivity.ERROR,
|
||||||
|
STATE_PAUSED: VacuumActivity.PAUSED,
|
||||||
|
STATE_RETURNING: VacuumActivity.RETURNING,
|
||||||
|
STATE_CLEANING: VacuumActivity.CLEANING,
|
||||||
}
|
}
|
||||||
|
|
||||||
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
|
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
|
||||||
|
@ -263,7 +262,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||||
if STATE in payload and (
|
if STATE in payload and (
|
||||||
(state := payload[STATE]) in POSSIBLE_STATES or state is None
|
(state := payload[STATE]) in POSSIBLE_STATES or state is None
|
||||||
):
|
):
|
||||||
self._attr_state = (
|
self._attr_activity = (
|
||||||
POSSIBLE_STATES[cast(str, state)] if payload[STATE] else None
|
POSSIBLE_STATES[cast(str, state)] if payload[STATE] else None
|
||||||
)
|
)
|
||||||
del payload[STATE]
|
del payload[STATE]
|
||||||
|
@ -275,7 +274,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||||
self.add_subscription(
|
self.add_subscription(
|
||||||
CONF_STATE_TOPIC,
|
CONF_STATE_TOPIC,
|
||||||
self._state_message_received,
|
self._state_message_received,
|
||||||
{"_attr_battery_level", "_attr_fan_speed", "_attr_state"},
|
{"_attr_battery_level", "_attr_fan_speed", "_attr_activity"},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _subscribe_topics(self) -> None:
|
async def _subscribe_topics(self) -> None:
|
||||||
|
|
|
@ -4,8 +4,9 @@
|
||||||
"after_dependencies": ["media_source", "media_player"],
|
"after_dependencies": ["media_source", "media_player"],
|
||||||
"codeowners": ["@music-assistant"],
|
"codeowners": ["@music-assistant"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
|
"documentation": "https://music-assistant.io",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
"issue_tracker": "https://github.com/music-assistant/hass-music-assistant/issues",
|
||||||
"loggers": ["music_assistant"],
|
"loggers": ["music_assistant"],
|
||||||
"requirements": ["music-assistant-client==1.0.5"],
|
"requirements": ["music-assistant-client==1.0.5"],
|
||||||
"zeroconf": ["_mass._tcp.local."]
|
"zeroconf": ["_mass._tcp.local."]
|
||||||
|
|
|
@ -12,15 +12,12 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
ATTR_STATUS,
|
ATTR_STATUS,
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_MODE, STATE_IDLE, STATE_PAUSED
|
from homeassistant.const import ATTR_MODE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
@ -169,23 +166,23 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||||
robot_alert = None
|
robot_alert = None
|
||||||
if self._state["state"] == 1:
|
if self._state["state"] == 1:
|
||||||
if self._state["details"]["isCharging"]:
|
if self._state["details"]["isCharging"]:
|
||||||
self._attr_state = STATE_DOCKED
|
self._attr_activity = VacuumActivity.DOCKED
|
||||||
self._status_state = "Charging"
|
self._status_state = "Charging"
|
||||||
elif (
|
elif (
|
||||||
self._state["details"]["isDocked"]
|
self._state["details"]["isDocked"]
|
||||||
and not self._state["details"]["isCharging"]
|
and not self._state["details"]["isCharging"]
|
||||||
):
|
):
|
||||||
self._attr_state = STATE_DOCKED
|
self._attr_activity = VacuumActivity.DOCKED
|
||||||
self._status_state = "Docked"
|
self._status_state = "Docked"
|
||||||
else:
|
else:
|
||||||
self._attr_state = STATE_IDLE
|
self._attr_activity = VacuumActivity.IDLE
|
||||||
self._status_state = "Stopped"
|
self._status_state = "Stopped"
|
||||||
|
|
||||||
if robot_alert is not None:
|
if robot_alert is not None:
|
||||||
self._status_state = robot_alert
|
self._status_state = robot_alert
|
||||||
elif self._state["state"] == 2:
|
elif self._state["state"] == 2:
|
||||||
if robot_alert is None:
|
if robot_alert is None:
|
||||||
self._attr_state = STATE_CLEANING
|
self._attr_activity = VacuumActivity.CLEANING
|
||||||
self._status_state = (
|
self._status_state = (
|
||||||
f"{MODE.get(self._state['cleaning']['mode'])} "
|
f"{MODE.get(self._state['cleaning']['mode'])} "
|
||||||
f"{ACTION.get(self._state['action'])}"
|
f"{ACTION.get(self._state['action'])}"
|
||||||
|
@ -200,10 +197,10 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||||
else:
|
else:
|
||||||
self._status_state = robot_alert
|
self._status_state = robot_alert
|
||||||
elif self._state["state"] == 3:
|
elif self._state["state"] == 3:
|
||||||
self._attr_state = STATE_PAUSED
|
self._attr_activity = VacuumActivity.PAUSED
|
||||||
self._status_state = "Paused"
|
self._status_state = "Paused"
|
||||||
elif self._state["state"] == 4:
|
elif self._state["state"] == 4:
|
||||||
self._attr_state = STATE_ERROR
|
self._attr_activity = VacuumActivity.ERROR
|
||||||
self._status_state = ERRORS.get(self._state["error"])
|
self._status_state = ERRORS.get(self._state["error"])
|
||||||
|
|
||||||
self._attr_battery_level = self._state["details"]["charge"]
|
self._attr_battery_level = self._state["details"]["charge"]
|
||||||
|
@ -326,9 +323,9 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||||
def return_to_base(self, **kwargs: Any) -> None:
|
def return_to_base(self, **kwargs: Any) -> None:
|
||||||
"""Set the vacuum cleaner to return to the dock."""
|
"""Set the vacuum cleaner to return to the dock."""
|
||||||
try:
|
try:
|
||||||
if self._attr_state == STATE_CLEANING:
|
if self._attr_activity == VacuumActivity.CLEANING:
|
||||||
self.robot.pause_cleaning()
|
self.robot.pause_cleaning()
|
||||||
self._attr_state = STATE_RETURNING
|
self._attr_activity = VacuumActivity.RETURNING
|
||||||
self.robot.send_to_base()
|
self.robot.send_to_base()
|
||||||
except NeatoRobotException as ex:
|
except NeatoRobotException as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -380,7 +377,7 @@ class NeatoConnectedVacuum(NeatoEntity, StateVacuumEntity):
|
||||||
"Start cleaning zone '%s' with robot %s", zone, self.entity_id
|
"Start cleaning zone '%s' with robot %s", zone, self.entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_state = STATE_CLEANING
|
self._attr_activity = VacuumActivity.CLEANING
|
||||||
try:
|
try:
|
||||||
self.robot.start_cleaning(mode, navigation, category, boundary_id)
|
self.robot.start_cleaning(mode, navigation, category, boundary_id)
|
||||||
except NeatoRobotException as ex:
|
except NeatoRobotException as ex:
|
||||||
|
|
|
@ -4,12 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pynordpool import (
|
from pynordpool import Currency, NordPoolClient, NordPoolError
|
||||||
Currency,
|
|
||||||
NordPoolClient,
|
|
||||||
NordPoolEmptyResponseError,
|
|
||||||
NordPoolError,
|
|
||||||
)
|
|
||||||
from pynordpool.const import AREAS
|
from pynordpool.const import AREAS
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -58,16 +53,17 @@ async def test_api(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str,
|
||||||
"""Test fetch data from Nord Pool."""
|
"""Test fetch data from Nord Pool."""
|
||||||
client = NordPoolClient(async_get_clientsession(hass))
|
client = NordPoolClient(async_get_clientsession(hass))
|
||||||
try:
|
try:
|
||||||
await client.async_get_delivery_period(
|
data = await client.async_get_delivery_period(
|
||||||
dt_util.now(),
|
dt_util.now(),
|
||||||
Currency(user_input[CONF_CURRENCY]),
|
Currency(user_input[CONF_CURRENCY]),
|
||||||
user_input[CONF_AREAS],
|
user_input[CONF_AREAS],
|
||||||
)
|
)
|
||||||
except NordPoolEmptyResponseError:
|
|
||||||
return {"base": "no_data"}
|
|
||||||
except NordPoolError:
|
except NordPoolError:
|
||||||
return {"base": "cannot_connect"}
|
return {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
if not data.raw:
|
||||||
|
return {"base": "no_data"}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ from typing import TYPE_CHECKING
|
||||||
from pynordpool import (
|
from pynordpool import (
|
||||||
Currency,
|
Currency,
|
||||||
DeliveryPeriodData,
|
DeliveryPeriodData,
|
||||||
|
NordPoolAuthenticationError,
|
||||||
NordPoolClient,
|
NordPoolClient,
|
||||||
NordPoolEmptyResponseError,
|
|
||||||
NordPoolError,
|
NordPoolError,
|
||||||
NordPoolResponseError,
|
NordPoolResponseError,
|
||||||
)
|
)
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.const import CONF_CURRENCY
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import CONF_AREAS, DOMAIN, LOGGER
|
from .const import CONF_AREAS, DOMAIN, LOGGER
|
||||||
|
@ -75,8 +75,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]):
|
||||||
Currency(self.config_entry.data[CONF_CURRENCY]),
|
Currency(self.config_entry.data[CONF_CURRENCY]),
|
||||||
self.config_entry.data[CONF_AREAS],
|
self.config_entry.data[CONF_AREAS],
|
||||||
)
|
)
|
||||||
except NordPoolEmptyResponseError as error:
|
except NordPoolAuthenticationError as error:
|
||||||
LOGGER.debug("Empty response error: %s", error)
|
LOGGER.error("Authentication error: %s", error)
|
||||||
self.async_set_update_error(error)
|
self.async_set_update_error(error)
|
||||||
return
|
return
|
||||||
except NordPoolResponseError as error:
|
except NordPoolResponseError as error:
|
||||||
|
@ -88,4 +88,8 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodData]):
|
||||||
self.async_set_update_error(error)
|
self.async_set_update_error(error)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not data.raw:
|
||||||
|
self.async_set_update_error(UpdateFailed("No data"))
|
||||||
|
return
|
||||||
|
|
||||||
self.async_set_updated_data(data)
|
self.async_set_updated_data(data)
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
"""Diagnostics support for Nord Pool."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from . import NordPoolConfigEntry
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
|
||||||
hass: HomeAssistant, entry: NordPoolConfigEntry
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return diagnostics for Nord Pool config entry."""
|
|
||||||
return {"raw": entry.runtime_data.data.raw}
|
|
|
@ -28,7 +28,7 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.unit_conversion import (
|
from homeassistant.util.unit_conversion import (
|
||||||
BaseUnitConverter,
|
BaseUnitConverter,
|
||||||
BloodGlucoseConcentrationConverter,
|
BloodGlugoseConcentrationConverter,
|
||||||
ConductivityConverter,
|
ConductivityConverter,
|
||||||
DataRateConverter,
|
DataRateConverter,
|
||||||
DistanceConverter,
|
DistanceConverter,
|
||||||
|
@ -130,8 +130,8 @@ QUERY_STATISTICS_SUMMARY_SUM = (
|
||||||
|
|
||||||
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
|
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
|
||||||
**{
|
**{
|
||||||
unit: BloodGlucoseConcentrationConverter
|
unit: BloodGlugoseConcentrationConverter
|
||||||
for unit in BloodGlucoseConcentrationConverter.VALID_UNITS
|
for unit in BloodGlugoseConcentrationConverter.VALID_UNITS
|
||||||
},
|
},
|
||||||
**{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS},
|
**{unit: ConductivityConverter for unit in ConductivityConverter.VALID_UNITS},
|
||||||
**{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS},
|
**{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS},
|
||||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.json import json_bytes
|
from homeassistant.helpers.json import json_bytes
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.util.unit_conversion import (
|
from homeassistant.util.unit_conversion import (
|
||||||
BloodGlucoseConcentrationConverter,
|
BloodGlugoseConcentrationConverter,
|
||||||
ConductivityConverter,
|
ConductivityConverter,
|
||||||
DataRateConverter,
|
DataRateConverter,
|
||||||
DistanceConverter,
|
DistanceConverter,
|
||||||
|
@ -56,7 +56,7 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10
|
||||||
UNIT_SCHEMA = vol.Schema(
|
UNIT_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional("blood_glucose_concentration"): vol.In(
|
vol.Optional("blood_glucose_concentration"): vol.In(
|
||||||
BloodGlucoseConcentrationConverter.VALID_UNITS
|
BloodGlugoseConcentrationConverter.VALID_UNITS
|
||||||
),
|
),
|
||||||
vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS),
|
vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS),
|
||||||
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
|
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
|
||||||
|
|
|
@ -18,5 +18,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"requirements": ["reolink-aio==0.11.1"]
|
"requirements": ["reolink-aio==0.10.4"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
if (alert := self._get_coordinator_alert()) and not alert.is_update:
|
if alert := self._get_coordinator_alert():
|
||||||
self._async_handle_event(alert.kind)
|
self._async_handle_event(alert.kind)
|
||||||
super()._handle_coordinator_update()
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
|
|
@ -30,5 +30,5 @@
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["ring_doorbell"],
|
"loggers": ["ring_doorbell"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["ring-doorbell==0.9.12"]
|
"requirements": ["ring-doorbell==0.9.9"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -106,12 +107,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||||
async def _async_update_data(self) -> DeviceProp:
|
async def _async_update_data(self) -> DeviceProp:
|
||||||
"""Update data via library."""
|
"""Update data via library."""
|
||||||
try:
|
try:
|
||||||
# Update device props and standard api information
|
await asyncio.gather(*(self._update_device_prop(), self.get_rooms()))
|
||||||
await self._update_device_prop()
|
|
||||||
# Set the new map id from the updated device props
|
|
||||||
self._set_current_map()
|
self._set_current_map()
|
||||||
# Get the rooms for that map id.
|
|
||||||
await self.get_rooms()
|
|
||||||
except RoborockException as ex:
|
except RoborockException as ex:
|
||||||
raise UpdateFailed(ex) from ex
|
raise UpdateFailed(ex) from ex
|
||||||
return self.roborock_device_info.props
|
return self.roborock_device_info.props
|
||||||
|
|
|
@ -135,9 +135,6 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||||
RoborockCommand.LOAD_MULTI_MAP,
|
RoborockCommand.LOAD_MULTI_MAP,
|
||||||
[map_id],
|
[map_id],
|
||||||
)
|
)
|
||||||
# Update the current map id manually so that nothing gets broken
|
|
||||||
# if another service hits the api.
|
|
||||||
self.coordinator.current_map = map_id
|
|
||||||
# We need to wait after updating the map
|
# We need to wait after updating the map
|
||||||
# so that other commands will be executed correctly.
|
# so that other commands will be executed correctly.
|
||||||
await asyncio.sleep(MAP_SLEEP)
|
await asyncio.sleep(MAP_SLEEP)
|
||||||
|
@ -151,9 +148,6 @@ class RoborockCurrentMapSelectEntity(RoborockCoordinatedEntityV1, SelectEntity):
|
||||||
@property
|
@property
|
||||||
def current_option(self) -> str | None:
|
def current_option(self) -> str | None:
|
||||||
"""Get the current status of the select entity from device_status."""
|
"""Get the current status of the select entity from device_status."""
|
||||||
if (
|
if (current_map := self.coordinator.current_map) is not None:
|
||||||
(current_map := self.coordinator.current_map) is not None
|
|
||||||
and current_map in self.coordinator.maps
|
|
||||||
): # 63 means it is searching for a map.
|
|
||||||
return self.coordinator.maps[current_map].name
|
return self.coordinator.maps[current_map].name
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -8,13 +8,8 @@ from roborock.roborock_message import RoborockDataProtocol
|
||||||
from roborock.roborock_typing import RoborockCommand
|
from roborock.roborock_typing import RoborockCommand
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||||
|
@ -27,29 +22,29 @@ from .coordinator import RoborockDataUpdateCoordinator
|
||||||
from .entity import RoborockCoordinatedEntityV1
|
from .entity import RoborockCoordinatedEntityV1
|
||||||
|
|
||||||
STATE_CODE_TO_STATE = {
|
STATE_CODE_TO_STATE = {
|
||||||
RoborockStateCode.starting: STATE_IDLE, # "Starting"
|
RoborockStateCode.starting: VacuumActivity.IDLE, # "Starting"
|
||||||
RoborockStateCode.charger_disconnected: STATE_IDLE, # "Charger disconnected"
|
RoborockStateCode.charger_disconnected: VacuumActivity.IDLE, # "Charger disconnected"
|
||||||
RoborockStateCode.idle: STATE_IDLE, # "Idle"
|
RoborockStateCode.idle: VacuumActivity.IDLE, # "Idle"
|
||||||
RoborockStateCode.remote_control_active: STATE_CLEANING, # "Remote control active"
|
RoborockStateCode.remote_control_active: VacuumActivity.CLEANING, # "Remote control active"
|
||||||
RoborockStateCode.cleaning: STATE_CLEANING, # "Cleaning"
|
RoborockStateCode.cleaning: VacuumActivity.CLEANING, # "Cleaning"
|
||||||
RoborockStateCode.returning_home: STATE_RETURNING, # "Returning home"
|
RoborockStateCode.returning_home: VacuumActivity.RETURNING, # "Returning home"
|
||||||
RoborockStateCode.manual_mode: STATE_CLEANING, # "Manual mode"
|
RoborockStateCode.manual_mode: VacuumActivity.CLEANING, # "Manual mode"
|
||||||
RoborockStateCode.charging: STATE_DOCKED, # "Charging"
|
RoborockStateCode.charging: VacuumActivity.DOCKED, # "Charging"
|
||||||
RoborockStateCode.charging_problem: STATE_ERROR, # "Charging problem"
|
RoborockStateCode.charging_problem: VacuumActivity.ERROR, # "Charging problem"
|
||||||
RoborockStateCode.paused: STATE_PAUSED, # "Paused"
|
RoborockStateCode.paused: VacuumActivity.PAUSED, # "Paused"
|
||||||
RoborockStateCode.spot_cleaning: STATE_CLEANING, # "Spot cleaning"
|
RoborockStateCode.spot_cleaning: VacuumActivity.CLEANING, # "Spot cleaning"
|
||||||
RoborockStateCode.error: STATE_ERROR, # "Error"
|
RoborockStateCode.error: VacuumActivity.ERROR, # "Error"
|
||||||
RoborockStateCode.shutting_down: STATE_IDLE, # "Shutting down"
|
RoborockStateCode.shutting_down: VacuumActivity.IDLE, # "Shutting down"
|
||||||
RoborockStateCode.updating: STATE_DOCKED, # "Updating"
|
RoborockStateCode.updating: VacuumActivity.DOCKED, # "Updating"
|
||||||
RoborockStateCode.docking: STATE_RETURNING, # "Docking"
|
RoborockStateCode.docking: VacuumActivity.RETURNING, # "Docking"
|
||||||
RoborockStateCode.going_to_target: STATE_CLEANING, # "Going to target"
|
RoborockStateCode.going_to_target: VacuumActivity.CLEANING, # "Going to target"
|
||||||
RoborockStateCode.zoned_cleaning: STATE_CLEANING, # "Zoned cleaning"
|
RoborockStateCode.zoned_cleaning: VacuumActivity.CLEANING, # "Zoned cleaning"
|
||||||
RoborockStateCode.segment_cleaning: STATE_CLEANING, # "Segment cleaning"
|
RoborockStateCode.segment_cleaning: VacuumActivity.CLEANING, # "Segment cleaning"
|
||||||
RoborockStateCode.emptying_the_bin: STATE_DOCKED, # "Emptying the bin" on s7+
|
RoborockStateCode.emptying_the_bin: VacuumActivity.DOCKED, # "Emptying the bin" on s7+
|
||||||
RoborockStateCode.washing_the_mop: STATE_DOCKED, # "Washing the mop" on s7maxV
|
RoborockStateCode.washing_the_mop: VacuumActivity.DOCKED, # "Washing the mop" on s7maxV
|
||||||
RoborockStateCode.going_to_wash_the_mop: STATE_RETURNING, # "Going to wash the mop" on s7maxV
|
RoborockStateCode.going_to_wash_the_mop: VacuumActivity.RETURNING, # "Going to wash the mop" on s7maxV
|
||||||
RoborockStateCode.charging_complete: STATE_DOCKED, # "Charging complete"
|
RoborockStateCode.charging_complete: VacuumActivity.DOCKED, # "Charging complete"
|
||||||
RoborockStateCode.device_offline: STATE_ERROR, # "Device offline"
|
RoborockStateCode.device_offline: VacuumActivity.ERROR, # "Device offline"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,7 +107,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||||
self._attr_fan_speed_list = self._device_status.fan_power_options
|
self._attr_fan_speed_list = self._device_status.fan_power_options
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def activity(self) -> VacuumActivity | None:
|
||||||
"""Return the status of the vacuum cleaner."""
|
"""Return the status of the vacuum cleaner."""
|
||||||
assert self._device_status.state is not None
|
assert self._device_status.state is not None
|
||||||
return STATE_CODE_TO_STATE.get(self._device_status.state)
|
return STATE_CODE_TO_STATE.get(self._device_status.state)
|
||||||
|
|
|
@ -6,7 +6,11 @@ https://home-assistant.io/components/vacuum.romy/.
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.vacuum import StateVacuumEntity, VacuumEntityFeature
|
from homeassistant.components.vacuum import (
|
||||||
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
|
VacuumEntityFeature,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
@ -75,7 +79,11 @@ class RomyVacuumEntity(RomyEntity, StateVacuumEntity):
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
self._attr_fan_speed = FAN_SPEEDS[self.romy.fan_speed]
|
self._attr_fan_speed = FAN_SPEEDS[self.romy.fan_speed]
|
||||||
self._attr_battery_level = self.romy.battery_level
|
self._attr_battery_level = self.romy.battery_level
|
||||||
self._attr_state = self.romy.status
|
try:
|
||||||
|
assert self.romy.status is not None
|
||||||
|
self._attr_activity = VacuumActivity(self.romy.status)
|
||||||
|
except (AssertionError, ValueError):
|
||||||
|
self._attr_activity = None
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,11 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
ATTR_STATUS,
|
ATTR_STATUS,
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_CONNECTIONS, STATE_IDLE, STATE_PAUSED
|
from homeassistant.const import ATTR_CONNECTIONS
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
@ -46,16 +43,16 @@ SUPPORT_IROBOT = (
|
||||||
)
|
)
|
||||||
|
|
||||||
STATE_MAP = {
|
STATE_MAP = {
|
||||||
"": STATE_IDLE,
|
"": VacuumActivity.IDLE,
|
||||||
"charge": STATE_DOCKED,
|
"charge": VacuumActivity.DOCKED,
|
||||||
"evac": STATE_RETURNING, # Emptying at cleanbase
|
"evac": VacuumActivity.RETURNING, # Emptying at cleanbase
|
||||||
"hmMidMsn": STATE_CLEANING, # Recharging at the middle of a cycle
|
"hmMidMsn": VacuumActivity.CLEANING, # Recharging at the middle of a cycle
|
||||||
"hmPostMsn": STATE_RETURNING, # Cycle finished
|
"hmPostMsn": VacuumActivity.RETURNING, # Cycle finished
|
||||||
"hmUsrDock": STATE_RETURNING,
|
"hmUsrDock": VacuumActivity.RETURNING,
|
||||||
"pause": STATE_PAUSED,
|
"pause": VacuumActivity.PAUSED,
|
||||||
"run": STATE_CLEANING,
|
"run": VacuumActivity.CLEANING,
|
||||||
"stop": STATE_IDLE,
|
"stop": VacuumActivity.IDLE,
|
||||||
"stuck": STATE_ERROR,
|
"stuck": VacuumActivity.ERROR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,7 +125,7 @@ class IRobotEntity(Entity):
|
||||||
return dt_util.utc_from_timestamp(ts)
|
return dt_util.utc_from_timestamp(ts)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _robot_state(self):
|
def _robot_state(self) -> VacuumActivity:
|
||||||
"""Return the state of the vacuum cleaner."""
|
"""Return the state of the vacuum cleaner."""
|
||||||
clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {})
|
clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {})
|
||||||
cycle = clean_mission_status.get("cycle")
|
cycle = clean_mission_status.get("cycle")
|
||||||
|
@ -136,9 +133,12 @@ class IRobotEntity(Entity):
|
||||||
try:
|
try:
|
||||||
state = STATE_MAP[phase]
|
state = STATE_MAP[phase]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return STATE_ERROR
|
return VacuumActivity.ERROR
|
||||||
if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED):
|
if cycle != "none" and state in (
|
||||||
state = STATE_PAUSED
|
VacuumActivity.IDLE,
|
||||||
|
VacuumActivity.DOCKED,
|
||||||
|
):
|
||||||
|
state = VacuumActivity.PAUSED
|
||||||
return state
|
return state
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
|
@ -169,7 +169,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf
|
||||||
self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1
|
self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def activity(self) -> VacuumActivity:
|
||||||
"""Return the state of the vacuum cleaner."""
|
"""Return the state of the vacuum cleaner."""
|
||||||
return self._robot_state
|
return self._robot_state
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf
|
||||||
|
|
||||||
# Only add cleaning time and cleaned area attrs when the vacuum is
|
# Only add cleaning time and cleaned area attrs when the vacuum is
|
||||||
# currently on
|
# currently on
|
||||||
if self.state == STATE_CLEANING:
|
if self.state == VacuumActivity.CLEANING:
|
||||||
# Get clean mission status
|
# Get clean mission status
|
||||||
(
|
(
|
||||||
state_attrs[ATTR_CLEANING_TIME],
|
state_attrs[ATTR_CLEANING_TIME],
|
||||||
|
@ -243,7 +243,7 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf
|
||||||
|
|
||||||
async def async_start(self):
|
async def async_start(self):
|
||||||
"""Start or resume the cleaning task."""
|
"""Start or resume the cleaning task."""
|
||||||
if self.state == STATE_PAUSED:
|
if self.vacuum_state == VacuumActivity.PAUSED:
|
||||||
await self.hass.async_add_executor_job(self.vacuum.send_command, "resume")
|
await self.hass.async_add_executor_job(self.vacuum.send_command, "resume")
|
||||||
else:
|
else:
|
||||||
await self.hass.async_add_executor_job(self.vacuum.send_command, "start")
|
await self.hass.async_add_executor_job(self.vacuum.send_command, "start")
|
||||||
|
@ -258,10 +258,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): # pylint: disable=hass-enf
|
||||||
|
|
||||||
async def async_return_to_base(self, **kwargs):
|
async def async_return_to_base(self, **kwargs):
|
||||||
"""Set the vacuum cleaner to return to the dock."""
|
"""Set the vacuum cleaner to return to the dock."""
|
||||||
if self.state == STATE_CLEANING:
|
if self.vacuum_state == VacuumActivity.CLEANING:
|
||||||
await self.async_pause()
|
await self.async_pause()
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
if self.state == STATE_PAUSED:
|
if self.state == VacuumActivity.PAUSED:
|
||||||
break
|
break
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await self.hass.async_add_executor_job(self.vacuum.send_command, "dock")
|
await self.hass.async_add_executor_job(self.vacuum.send_command, "dock")
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioruckus"],
|
"loggers": ["aioruckus"],
|
||||||
"requirements": ["aioruckus==0.42"]
|
"requirements": ["aioruckus==0.41"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ from homeassistant.helpers.deprecation import (
|
||||||
)
|
)
|
||||||
from homeassistant.util.unit_conversion import (
|
from homeassistant.util.unit_conversion import (
|
||||||
BaseUnitConverter,
|
BaseUnitConverter,
|
||||||
BloodGlucoseConcentrationConverter,
|
BloodGlugoseConcentrationConverter,
|
||||||
ConductivityConverter,
|
ConductivityConverter,
|
||||||
DataRateConverter,
|
DataRateConverter,
|
||||||
DistanceConverter,
|
DistanceConverter,
|
||||||
|
@ -501,7 +501,7 @@ STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
|
||||||
|
|
||||||
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
|
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
|
||||||
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
|
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
|
||||||
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter,
|
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlugoseConcentrationConverter,
|
||||||
SensorDeviceClass.CONDUCTIVITY: ConductivityConverter,
|
SensorDeviceClass.CONDUCTIVITY: ConductivityConverter,
|
||||||
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
|
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
|
||||||
SensorDeviceClass.DATA_RATE: DataRateConverter,
|
SensorDeviceClass.DATA_RATE: DataRateConverter,
|
||||||
|
|
|
@ -9,12 +9,8 @@ from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -30,10 +26,10 @@ from .const import DOMAIN, LOGGER, SERVICE_CLEAN_ROOM, SHARK
|
||||||
from .coordinator import SharkIqUpdateCoordinator
|
from .coordinator import SharkIqUpdateCoordinator
|
||||||
|
|
||||||
OPERATING_STATE_MAP = {
|
OPERATING_STATE_MAP = {
|
||||||
OperatingModes.PAUSE: STATE_PAUSED,
|
OperatingModes.PAUSE: VacuumActivity.PAUSED,
|
||||||
OperatingModes.START: STATE_CLEANING,
|
OperatingModes.START: VacuumActivity.CLEANING,
|
||||||
OperatingModes.STOP: STATE_IDLE,
|
OperatingModes.STOP: VacuumActivity.IDLE,
|
||||||
OperatingModes.RETURN: STATE_RETURNING,
|
OperatingModes.RETURN: VacuumActivity.RETURNING,
|
||||||
}
|
}
|
||||||
|
|
||||||
FAN_SPEEDS_MAP = {
|
FAN_SPEEDS_MAP = {
|
||||||
|
@ -151,7 +147,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum
|
||||||
return self.sharkiq.error_text
|
return self.sharkiq.error_text
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def operating_mode(self) -> str | None:
|
def operating_mode(self) -> VacuumActivity | None:
|
||||||
"""Operating mode."""
|
"""Operating mode."""
|
||||||
op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE)
|
op_mode = self.sharkiq.get_property_value(Properties.OPERATING_MODE)
|
||||||
return OPERATING_STATE_MAP.get(op_mode)
|
return OPERATING_STATE_MAP.get(op_mode)
|
||||||
|
@ -162,7 +158,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum
|
||||||
return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME)
|
return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def activity(self) -> VacuumActivity | None:
|
||||||
"""Get the current vacuum state.
|
"""Get the current vacuum state.
|
||||||
|
|
||||||
NB: Currently, we do not return an error state because they can be very, very stale.
|
NB: Currently, we do not return an error state because they can be very, very stale.
|
||||||
|
@ -170,7 +166,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum
|
||||||
user a notification.
|
user a notification.
|
||||||
"""
|
"""
|
||||||
if self.sharkiq.get_property_value(Properties.CHARGING_STATUS):
|
if self.sharkiq.get_property_value(Properties.CHARGING_STATUS):
|
||||||
return STATE_DOCKED
|
return VacuumActivity.DOCKED
|
||||||
return self.operating_mode
|
return self.operating_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
"""The Sky Remote Control integration."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from skyboxremote import RemoteControl, SkyBoxConnectionError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.REMOTE]
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
type SkyRemoteConfigEntry = ConfigEntry[RemoteControl]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: SkyRemoteConfigEntry) -> bool:
|
|
||||||
"""Set up Sky remote."""
|
|
||||||
host = entry.data[CONF_HOST]
|
|
||||||
port = entry.data[CONF_PORT]
|
|
||||||
|
|
||||||
_LOGGER.debug("Setting up Host: %s, Port: %s", host, port)
|
|
||||||
remote = RemoteControl(host, port)
|
|
||||||
try:
|
|
||||||
await remote.check_connectable()
|
|
||||||
except SkyBoxConnectionError as e:
|
|
||||||
raise ConfigEntryNotReady from e
|
|
||||||
|
|
||||||
entry.runtime_data = remote
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
@ -1,64 +0,0 @@
|
||||||
"""Config flow for sky_remote."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from skyboxremote import RemoteControl, SkyBoxConnectionError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
|
|
||||||
from .const import DEFAULT_PORT, DOMAIN, LEGACY_PORT
|
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_HOST): cv.string,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_find_box_port(host: str) -> int:
|
|
||||||
"""Find port box uses for communication."""
|
|
||||||
logging.debug("Attempting to find port to connect to %s on", host)
|
|
||||||
remote = RemoteControl(host, DEFAULT_PORT)
|
|
||||||
try:
|
|
||||||
await remote.check_connectable()
|
|
||||||
except SkyBoxConnectionError:
|
|
||||||
# Try legacy port if the default one failed
|
|
||||||
remote = RemoteControl(host, LEGACY_PORT)
|
|
||||||
await remote.check_connectable()
|
|
||||||
return LEGACY_PORT
|
|
||||||
return DEFAULT_PORT
|
|
||||||
|
|
||||||
|
|
||||||
class SkyRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for Sky Remote."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
MINOR_VERSION = 1
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the user step."""
|
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
if user_input is not None:
|
|
||||||
logging.debug("user_input: %s", user_input)
|
|
||||||
self._async_abort_entries_match(user_input)
|
|
||||||
try:
|
|
||||||
port = await async_find_box_port(user_input[CONF_HOST])
|
|
||||||
except SkyBoxConnectionError:
|
|
||||||
logging.exception("while finding port of skybox")
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
else:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=user_input[CONF_HOST],
|
|
||||||
data={**user_input, CONF_PORT: port},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
"""Constants."""
|
|
||||||
|
|
||||||
DOMAIN = "sky_remote"
|
|
||||||
|
|
||||||
DEFAULT_PORT = 49160
|
|
||||||
LEGACY_PORT = 5900
|
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"domain": "sky_remote",
|
|
||||||
"name": "Sky Remote Control",
|
|
||||||
"codeowners": ["@dunnmj", "@saty9"],
|
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/sky_remote",
|
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "assumed_state",
|
|
||||||
"requirements": ["skyboxremote==0.0.6"]
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
"""Home Assistant integration to control a sky box using the remote platform."""
|
|
||||||
|
|
||||||
from collections.abc import Iterable
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from skyboxremote import VALID_KEYS, RemoteControl
|
|
||||||
|
|
||||||
from homeassistant.components.remote import RemoteEntity
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from . import SkyRemoteConfigEntry
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config: SkyRemoteConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the Sky remote platform."""
|
|
||||||
async_add_entities(
|
|
||||||
[SkyRemote(config.runtime_data, config.entry_id)],
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SkyRemote(RemoteEntity):
|
|
||||||
"""Representation of a Sky Remote."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_name = None
|
|
||||||
|
|
||||||
def __init__(self, remote: RemoteControl, unique_id: str) -> None:
|
|
||||||
"""Initialize the Sky Remote."""
|
|
||||||
self._remote = remote
|
|
||||||
self._attr_unique_id = unique_id
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, unique_id)},
|
|
||||||
manufacturer="SKY",
|
|
||||||
model="Sky Box",
|
|
||||||
name=remote.host,
|
|
||||||
)
|
|
||||||
|
|
||||||
def turn_on(self, activity: str | None = None, **kwargs: Any) -> None:
|
|
||||||
"""Send the power on command."""
|
|
||||||
self.send_command(["sky"])
|
|
||||||
|
|
||||||
def turn_off(self, activity: str | None = None, **kwargs: Any) -> None:
|
|
||||||
"""Send the power command."""
|
|
||||||
self.send_command(["power"])
|
|
||||||
|
|
||||||
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
|
|
||||||
"""Send a list of commands to the device."""
|
|
||||||
for cmd in command:
|
|
||||||
if cmd not in VALID_KEYS:
|
|
||||||
raise ServiceValidationError(
|
|
||||||
f"{cmd} is not in Valid Keys: {VALID_KEYS}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
self._remote.send_keys(command)
|
|
||||||
except ValueError as err:
|
|
||||||
_LOGGER.error("Invalid command: %s. Error: %s", command, err)
|
|
||||||
return
|
|
||||||
_LOGGER.debug("Successfully sent command %s", command)
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"title": "Add Sky Remote",
|
|
||||||
"data": {
|
|
||||||
"host": "[%key:common::config_flow::data::host%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "Hostname or IP address of your Sky device"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,10 +28,6 @@
|
||||||
"deprecated_yaml_import_issue_auth_error": {
|
"deprecated_yaml_import_issue_auth_error": {
|
||||||
"title": "YAML import failed due to an authentication error",
|
"title": "YAML import failed due to an authentication error",
|
||||||
"description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
"description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||||
},
|
|
||||||
"deprecated_yaml_import_issue_cannot_connect": {
|
|
||||||
"title": "YAML import failed due to a connection error",
|
|
||||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
|
|
@ -17,8 +17,7 @@
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||||
"unknown": "Unexpected error"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
|
|
@ -23,10 +23,10 @@
|
||||||
"state_characteristic": {
|
"state_characteristic": {
|
||||||
"description": "Read the documention for further details on available options and how to use them.",
|
"description": "Read the documention for further details on available options and how to use them.",
|
||||||
"data": {
|
"data": {
|
||||||
"state_characteristic": "Statistic characteristic"
|
"state_characteristic": "State_characteristic"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"state_characteristic": "The statistic characteristic that should be used as the state of the sensor."
|
"state_characteristic": "The characteristic that should be used as the state of the statistics sensor."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|
|
@ -37,13 +37,13 @@
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"incorrect_pin": "Incorrect PIN",
|
"incorrect_pin": "Incorrect PIN",
|
||||||
"bad_pin_format": "PIN should be 4 digits",
|
"bad_pin_format": "PIN should be 4 digits",
|
||||||
|
"two_factor_request_failed": "Request for 2FA code failed, please try again",
|
||||||
"bad_validation_code_format": "Validation code should be 6 digits",
|
"bad_validation_code_format": "Validation code should be 6 digits",
|
||||||
"incorrect_validation_code": "Incorrect validation code"
|
"incorrect_validation_code": "Incorrect validation code"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
"two_factor_request_failed": "Request for 2FA code failed, please try again"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|
|
@ -5,13 +5,8 @@ from typing import Any
|
||||||
from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands
|
from switchbot_api import Device, Remote, SwitchBotAPI, VacuumCommands
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -43,17 +38,17 @@ async def async_setup_entry(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, str] = {
|
VACUUM_SWITCHBOT_STATE_TO_HA_STATE: dict[str, VacuumActivity] = {
|
||||||
"StandBy": STATE_IDLE,
|
"StandBy": VacuumActivity.IDLE,
|
||||||
"Clearing": STATE_CLEANING,
|
"Clearing": VacuumActivity.CLEANING,
|
||||||
"Paused": STATE_PAUSED,
|
"Paused": VacuumActivity.PAUSED,
|
||||||
"GotoChargeBase": STATE_RETURNING,
|
"GotoChargeBase": VacuumActivity.RETURNING,
|
||||||
"Charging": STATE_DOCKED,
|
"Charging": VacuumActivity.DOCKED,
|
||||||
"ChargeDone": STATE_DOCKED,
|
"ChargeDone": VacuumActivity.DOCKED,
|
||||||
"Dormant": STATE_IDLE,
|
"Dormant": VacuumActivity.IDLE,
|
||||||
"InTrouble": STATE_ERROR,
|
"InTrouble": VacuumActivity.ERROR,
|
||||||
"InRemoteControl": STATE_CLEANING,
|
"InRemoteControl": VacuumActivity.CLEANING,
|
||||||
"InDustCollecting": STATE_DOCKED,
|
"InDustCollecting": VacuumActivity.DOCKED,
|
||||||
}
|
}
|
||||||
|
|
||||||
VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = {
|
VACUUM_FAN_SPEED_TO_SWITCHBOT_FAN_SPEED: dict[str, str] = {
|
||||||
|
@ -114,7 +109,7 @@ class SwitchBotCloudVacuum(SwitchBotCloudEntity, StateVacuumEntity):
|
||||||
self._attr_available = self.coordinator.data.get("onlineStatus") == "online"
|
self._attr_available = self.coordinator.data.get("onlineStatus") == "online"
|
||||||
|
|
||||||
switchbot_state = str(self.coordinator.data.get("workingStatus"))
|
switchbot_state = str(self.coordinator.data.get("workingStatus"))
|
||||||
self._attr_state = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get(switchbot_state)
|
self._attr_activity = VACUUM_SWITCHBOT_STATE_TO_HA_STATE.get(switchbot_state)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "template",
|
"domain": "template",
|
||||||
"name": "Template",
|
"name": "Template",
|
||||||
"after_dependencies": ["group"],
|
"after_dependencies": ["group"],
|
||||||
"codeowners": ["@PhracturedBlue", "@home-assistant/core"],
|
"codeowners": ["@PhracturedBlue", "@tetienne", "@home-assistant/core"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["blueprint"],
|
"dependencies": ["blueprint"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/template",
|
"documentation": "https://www.home-assistant.io/integrations/template",
|
||||||
|
|
|
@ -17,13 +17,8 @@ from homeassistant.components.vacuum import (
|
||||||
SERVICE_SET_FAN_SPEED,
|
SERVICE_SET_FAN_SPEED,
|
||||||
SERVICE_START,
|
SERVICE_START,
|
||||||
SERVICE_STOP,
|
SERVICE_STOP,
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_ERROR,
|
|
||||||
STATE_IDLE,
|
|
||||||
STATE_PAUSED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -58,12 +53,12 @@ CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
|
||||||
|
|
||||||
ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}"
|
ENTITY_ID_FORMAT = VACUUM_DOMAIN + ".{}"
|
||||||
_VALID_STATES = [
|
_VALID_STATES = [
|
||||||
STATE_CLEANING,
|
VacuumActivity.CLEANING,
|
||||||
STATE_DOCKED,
|
VacuumActivity.DOCKED,
|
||||||
STATE_PAUSED,
|
VacuumActivity.PAUSED,
|
||||||
STATE_IDLE,
|
VacuumActivity.IDLE,
|
||||||
STATE_RETURNING,
|
VacuumActivity.RETURNING,
|
||||||
STATE_ERROR,
|
VacuumActivity.ERROR,
|
||||||
]
|
]
|
||||||
|
|
||||||
VACUUM_SCHEMA = vol.All(
|
VACUUM_SCHEMA = vol.All(
|
||||||
|
@ -202,7 +197,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity):
|
||||||
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def activity(self) -> VacuumActivity | None:
|
||||||
"""Return the status of the vacuum cleaner."""
|
"""Return the status of the vacuum cleaner."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,6 @@ class TeslaSystemImplementation(config_entry_oauth2_flow.LocalOAuth2Implementati
|
||||||
def extra_authorize_data(self) -> dict[str, Any]:
|
def extra_authorize_data(self) -> dict[str, Any]:
|
||||||
"""Extra data that needs to be appended to the authorize url."""
|
"""Extra data that needs to be appended to the authorize url."""
|
||||||
return {
|
return {
|
||||||
"prompt": "login",
|
|
||||||
"scope": " ".join(SCOPES),
|
"scope": " ".join(SCOPES),
|
||||||
"code_challenge": self.code_challenge, # PKCE
|
"code_challenge": self.code_challenge, # PKCE
|
||||||
}
|
}
|
||||||
|
@ -84,4 +83,4 @@ class TeslaUserImplementation(AuthImplementation):
|
||||||
@property
|
@property
|
||||||
def extra_authorize_data(self) -> dict[str, Any]:
|
def extra_authorize_data(self) -> dict[str, Any]:
|
||||||
"""Extra data that needs to be appended to the authorize url."""
|
"""Extra data that needs to be appended to the authorize url."""
|
||||||
return {"prompt": "login", "scope": " ".join(SCOPES)}
|
return {"scope": " ".join(SCOPES)}
|
||||||
|
|
|
@ -7,13 +7,10 @@ from typing import Any
|
||||||
from tuya_sharing import CustomerDevice, Manager
|
from tuya_sharing import CustomerDevice, Manager
|
||||||
|
|
||||||
from homeassistant.components.vacuum import (
|
from homeassistant.components.vacuum import (
|
||||||
STATE_CLEANING,
|
|
||||||
STATE_DOCKED,
|
|
||||||
STATE_RETURNING,
|
|
||||||
StateVacuumEntity,
|
StateVacuumEntity,
|
||||||
|
VacuumActivity,
|
||||||
VacuumEntityFeature,
|
VacuumEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_IDLE, STATE_PAUSED
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
@ -24,29 +21,29 @@ from .entity import EnumTypeData, IntegerTypeData, TuyaEntity
|
||||||
|
|
||||||
TUYA_MODE_RETURN_HOME = "chargego"
|
TUYA_MODE_RETURN_HOME = "chargego"
|
||||||
TUYA_STATUS_TO_HA = {
|
TUYA_STATUS_TO_HA = {
|
||||||
"charge_done": STATE_DOCKED,
|
"charge_done": VacuumActivity.DOCKED,
|
||||||
"chargecompleted": STATE_DOCKED,
|
"chargecompleted": VacuumActivity.DOCKED,
|
||||||
"chargego": STATE_DOCKED,
|
"chargego": VacuumActivity.DOCKED,
|
||||||
"charging": STATE_DOCKED,
|
"charging": VacuumActivity.DOCKED,
|
||||||
"cleaning": STATE_CLEANING,
|
"cleaning": VacuumActivity.CLEANING,
|
||||||
"docking": STATE_RETURNING,
|
"docking": VacuumActivity.RETURNING,
|
||||||
"goto_charge": STATE_RETURNING,
|
"goto_charge": VacuumActivity.RETURNING,
|
||||||
"goto_pos": STATE_CLEANING,
|
"goto_pos": VacuumActivity.CLEANING,
|
||||||
"mop_clean": STATE_CLEANING,
|
"mop_clean": VacuumActivity.CLEANING,
|
||||||
"part_clean": STATE_CLEANING,
|
"part_clean": VacuumActivity.CLEANING,
|
||||||
"paused": STATE_PAUSED,
|
"paused": VacuumActivity.PAUSED,
|
||||||
"pick_zone_clean": STATE_CLEANING,
|
"pick_zone_clean": VacuumActivity.CLEANING,
|
||||||
"pos_arrived": STATE_CLEANING,
|
"pos_arrived": VacuumActivity.CLEANING,
|
||||||
"pos_unarrive": STATE_CLEANING,
|
"pos_unarrive": VacuumActivity.CLEANING,
|
||||||
"random": STATE_CLEANING,
|
"random": VacuumActivity.CLEANING,
|
||||||
"sleep": STATE_IDLE,
|
"sleep": VacuumActivity.IDLE,
|
||||||
"smart_clean": STATE_CLEANING,
|
"smart_clean": VacuumActivity.CLEANING,
|
||||||
"smart": STATE_CLEANING,
|
"smart": VacuumActivity.CLEANING,
|
||||||
"spot_clean": STATE_CLEANING,
|
"spot_clean": VacuumActivity.CLEANING,
|
||||||
"standby": STATE_IDLE,
|
"standby": VacuumActivity.IDLE,
|
||||||
"wall_clean": STATE_CLEANING,
|
"wall_clean": VacuumActivity.CLEANING,
|
||||||
"wall_follow": STATE_CLEANING,
|
"wall_follow": VacuumActivity.CLEANING,
|
||||||
"zone_clean": STATE_CLEANING,
|
"zone_clean": VacuumActivity.CLEANING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -137,12 +134,12 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
||||||
return self.device.status.get(DPCode.SUCTION)
|
return self.device.status.get(DPCode.SUCTION)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def activity(self) -> VacuumActivity | None:
|
||||||
"""Return Tuya vacuum device state."""
|
"""Return Tuya vacuum device state."""
|
||||||
if self.device.status.get(DPCode.PAUSE) and not (
|
if self.device.status.get(DPCode.PAUSE) and not (
|
||||||
self.device.status.get(DPCode.STATUS)
|
self.device.status.get(DPCode.STATUS)
|
||||||
):
|
):
|
||||||
return STATE_PAUSED
|
return VacuumActivity.PAUSED
|
||||||
if not (status := self.device.status.get(DPCode.STATUS)):
|
if not (status := self.device.status.get(DPCode.STATUS)):
|
||||||
return None
|
return None
|
||||||
return TUYA_STATUS_TO_HA.get(status)
|
return TUYA_STATUS_TO_HA.get(status)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue