Compare commits

..

4 Commits

Author SHA1 Message Date
Abílio Costa
5bd912c730 Merge branch 'dev' into llm_device_name 2025-09-24 11:50:37 +01:00
Abílio Costa
ad9efd6429 Merge branch 'dev' into llm_device_name 2025-09-22 10:29:46 +01:00
abmantis
3b59a03dfa Add device name to llm exposed entities info 2025-09-22 00:57:35 +01:00
abmantis
78bf54de42 Remove unused var from llm helper 2025-09-22 00:55:48 +01:00
129 changed files with 1179 additions and 5068 deletions

View File

@@ -58,7 +58,6 @@ base_platforms: &base_platforms
# Extra components that trigger the full suite
components: &components
- homeassistant/components/alexa/**
- homeassistant/components/analytics/**
- homeassistant/components/application_credentials/**
- homeassistant/components/assist_pipeline/**
- homeassistant/components/auth/**

View File

@@ -443,7 +443,6 @@ homeassistant.components.rituals_perfume_genie.*
homeassistant.components.roborock.*
homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.route_b_smart_meter.*
homeassistant.components.rpi_power.*
homeassistant.components.rss_feed_template.*
homeassistant.components.russound_rio.*

4
CODEOWNERS generated
View File

@@ -316,8 +316,6 @@ build.json @home-assistant/supervisor
/tests/components/crownstone/ @Crownstone @RicArch97
/homeassistant/components/cups/ @fabaff
/tests/components/cups/ @fabaff
/homeassistant/components/cync/ @Kinachi249
/tests/components/cync/ @Kinachi249
/homeassistant/components/daikin/ @fredrike
/tests/components/daikin/ @fredrike
/homeassistant/components/date/ @home-assistant/core
@@ -1334,8 +1332,6 @@ build.json @home-assistant/supervisor
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
/homeassistant/components/roon/ @pavoni
/tests/components/roon/ @pavoni
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
/tests/components/route_b_smart_meter/ @SeraphicRav
/homeassistant/components/rpi_power/ @shenxn @swetoast
/tests/components/rpi_power/ @shenxn @swetoast
/homeassistant/components/rss_feed_template/ @home-assistant/core

View File

@@ -4,9 +4,11 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import cast
from aioacaia.acaiascale import AcaiaScale
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
from bleak import BleakScanner
from homeassistant.components.bluetooth import async_get_scanner
from homeassistant.config_entries import ConfigEntry
@@ -43,7 +45,7 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
name=entry.title,
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
notify_callback=self.async_update_listeners,
scanner=async_get_scanner(hass),
scanner=cast(BleakScanner, async_get_scanner(hass)),
)
@property

View File

@@ -6,19 +6,17 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Final
from aioairzone.common import GrilleAngle, OperationMode, QAdapt, SleepTimeout
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
from aioairzone.const import (
API_COLD_ANGLE,
API_HEAT_ANGLE,
API_MODE,
API_Q_ADAPT,
API_SLEEP,
AZD_COLD_ANGLE,
AZD_HEAT_ANGLE,
AZD_MASTER,
AZD_MODE,
AZD_MODES,
AZD_Q_ADAPT,
AZD_SLEEP,
AZD_ZONES,
)
@@ -67,14 +65,6 @@ SLEEP_DICT: Final[dict[str, int]] = {
"90m": SleepTimeout.SLEEP_90,
}
Q_ADAPT_DICT: Final[dict[str, int]] = {
"standard": QAdapt.STANDARD,
"power": QAdapt.POWER,
"silence": QAdapt.SILENCE,
"minimum": QAdapt.MINIMUM,
"maximum": QAdapt.MAXIMUM,
}
def main_zone_options(
zone_data: dict[str, Any],
@@ -93,14 +83,6 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
options_fn=main_zone_options,
translation_key="modes",
),
AirzoneSelectDescription(
api_param=API_Q_ADAPT,
entity_category=EntityCategory.CONFIG,
key=AZD_Q_ADAPT,
options=list(Q_ADAPT_DICT),
options_dict=Q_ADAPT_DICT,
translation_key="q_adapt",
),
)

View File

@@ -63,16 +63,6 @@
"stop": "Stop"
}
},
"q_adapt": {
"name": "Q-Adapt",
"state": {
"standard": "Standard",
"power": "Power",
"silence": "Silence",
"minimum": "Minimum",
"maximum": "Maximum"
}
},
"sleep_times": {
"name": "Sleep",
"state": {

View File

@@ -94,24 +94,12 @@ async def async_setup_entry(
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in new_devices
if sensor_desc.is_supported(
coordinator.data[serial_num], sensor_desc.key
)
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in BINARY_SENSORS
for serial_num in coordinator.data
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
)
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):

View File

@@ -57,23 +57,13 @@ async def async_setup_entry(
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in new_devices
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in NOTIFY
for serial_num in coordinator.data
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):

View File

@@ -53,7 +53,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done

View File

@@ -62,22 +62,12 @@ async def async_setup_entry(
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in new_devices
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
for sensor_desc in SENSORS
for serial_num in coordinator.data
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
)
class AmazonSensorEntity(AmazonEntity, SensorEntity):

View File

@@ -48,22 +48,12 @@ async def async_setup_entry(
coordinator = entry.runtime_data
known_devices: set[str] = set()
def _check_device() -> None:
current_devices = set(coordinator.data)
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in new_devices
if switch_desc.subkey in coordinator.data[serial_num].capabilities
)
_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
for switch_desc in SWITCHES
for serial_num in coordinator.data
if switch_desc.subkey in coordinator.data[serial_num].capabilities
)
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):

View File

@@ -39,7 +39,7 @@ from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.typing import UNDEFINED
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.loader import (
Integration,
IntegrationNotFound,
@@ -142,6 +142,7 @@ class EntityAnalyticsModifications:
"""
remove: bool = False
capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED
class AnalyticsPlatformProtocol(Protocol):
@@ -676,14 +677,18 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
# we should replace it with the original value in the future.
# It is also not present, if entity is not in the state machine,
# which can happen for disabled entities.
"assumed_state": (
entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
if entity_state is not None
else None
),
"assumed_state": entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
if entity_state is not None
else None,
"capabilities": entity_config.capabilities
if entity_config.capabilities is not UNDEFINED
else entity_entry.capabilities,
"domain": entity_entry.domain,
"entity_category": entity_entry.entity_category,
"has_entity_name": entity_entry.has_entity_name,
"modified_by_integration": ["capabilities"]
if entity_config.capabilities is not UNDEFINED
else None,
"original_device_class": entity_entry.original_device_class,
# LIMITATION: `unit_of_measurement` can be overridden by users;
# we should replace it with the original value in the future.

View File

@@ -29,23 +29,10 @@ async def async_setup_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoBinarySensorEntity(
coordinator, device, config_entry.entry_id
)
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data["alarm_zones"].values()
)
class ComelitVedoBinarySensorEntity(

View File

@@ -29,21 +29,10 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[COVER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[COVER].values()
)
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):

View File

@@ -27,21 +27,10 @@ async def async_setup_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[LIGHT])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
async_add_entities(
ComelitLightEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[LIGHT].values()
)
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):

View File

@@ -57,7 +57,9 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
dynamic-devices:
status: todo
comment: missing implementation
entity-category:
status: exempt
comment: no config or diagnostic entities

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Final, cast
from aiocomelit.api import ComelitSerialBridgeObject, ComelitVedoZoneObject
from aiocomelit import ComelitSerialBridgeObject, ComelitVedoZoneObject
from aiocomelit.const import BRIDGE, OTHER, AlarmZoneState
from homeassistant.components.sensor import (
@@ -65,24 +65,15 @@ async def async_setup_bridge_entry(
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data[OTHER])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_BRIDGE_TYPES
for device in coordinator.data[OTHER].values()
if device.index in new_devices
entities: list[ComelitBridgeSensorEntity] = []
for device in coordinator.data[OTHER].values():
entities.extend(
ComelitBridgeSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
for sensor_desc in SENSOR_BRIDGE_TYPES
)
async_add_entities(entities)
async def async_setup_vedo_entry(
@@ -94,24 +85,15 @@ async def async_setup_vedo_entry(
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
known_devices: set[int] = set()
def _check_device() -> None:
current_devices = set(coordinator.data["alarm_zones"])
new_devices = current_devices - known_devices
if new_devices:
known_devices.update(new_devices)
async_add_entities(
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
for sensor_desc in SENSOR_VEDO_TYPES
for device in coordinator.data["alarm_zones"].values()
if device.index in new_devices
entities: list[ComelitVedoSensorEntity] = []
for device in coordinator.data["alarm_zones"].values():
entities.extend(
ComelitVedoSensorEntity(
coordinator, device, config_entry.entry_id, sensor_desc
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
for sensor_desc in SENSOR_VEDO_TYPES
)
async_add_entities(entities)
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):

View File

@@ -39,25 +39,6 @@ async def async_setup_entry(
)
async_add_entities(entities)
known_devices: dict[str, set[int]] = {
dev_type: set() for dev_type in (IRRIGATION, OTHER)
}
def _check_device() -> None:
for dev_type in (IRRIGATION, OTHER):
current_devices = set(coordinator.data[dev_type])
new_devices = current_devices - known_devices[dev_type]
if new_devices:
known_devices[dev_type].update(new_devices)
async_add_entities(
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
for device in coordinator.data[dev_type].values()
if device.index in new_devices
)
_check_device()
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
"""Switch device."""

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.24"]
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.3"]
}

View File

@@ -1,58 +0,0 @@
"""The Cync integration."""
from __future__ import annotations
from pycync import Auth, Cync, User
from pycync.exceptions import AuthFailedError, CyncError
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_AUTHORIZE_STRING,
CONF_EXPIRES_AT,
CONF_REFRESH_TOKEN,
CONF_USER_ID,
)
from .coordinator import CyncConfigEntry, CyncCoordinator
_PLATFORMS: list[Platform] = [Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CyncConfigEntry) -> bool:
"""Set up Cync from a config entry."""
user_info = User(
entry.data[CONF_ACCESS_TOKEN],
entry.data[CONF_REFRESH_TOKEN],
entry.data[CONF_AUTHORIZE_STRING],
entry.data[CONF_USER_ID],
expires_at=entry.data[CONF_EXPIRES_AT],
)
cync_auth = Auth(async_get_clientsession(hass), user=user_info)
try:
cync = await Cync.create(cync_auth)
except AuthFailedError as ex:
raise ConfigEntryAuthFailed("User token invalid") from ex
except CyncError as ex:
raise ConfigEntryNotReady("Unable to connect to Cync") from ex
devices_coordinator = CyncCoordinator(hass, entry, cync)
cync.set_update_callback(devices_coordinator.on_data_update)
await devices_coordinator.async_config_entry_first_refresh()
entry.runtime_data = devices_coordinator
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: CyncConfigEntry) -> bool:
"""Unload a config entry."""
cync = entry.runtime_data.cync
await cync.shut_down()
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@@ -1,118 +0,0 @@
"""Config flow for the Cync integration."""
from __future__ import annotations
import logging
from typing import Any
from pycync import Auth
from pycync.exceptions import AuthFailedError, CyncError, TwoFactorRequiredError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_AUTHORIZE_STRING,
CONF_EXPIRES_AT,
CONF_REFRESH_TOKEN,
CONF_TWO_FACTOR_CODE,
CONF_USER_ID,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
STEP_TWO_FACTOR_SCHEMA = vol.Schema({vol.Required(CONF_TWO_FACTOR_CODE): str})
class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cync."""
VERSION = 1
cync_auth: Auth
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Attempt login with user credentials."""
errors: dict[str, str] = {}
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login()
except AuthFailedError:
errors["base"] = "invalid_auth"
except TwoFactorRequiredError:
return await self.async_step_two_factor()
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_two_factor(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Attempt login with the two factor auth code sent to the user."""
errors: dict[str, str] = {}
if user_input is None:
return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
)
try:
await self.cync_auth.login(user_input[CONF_TWO_FACTOR_CODE])
except AuthFailedError:
errors["base"] = "invalid_auth"
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def _create_config_entry(self, user_email: str) -> ConfigFlowResult:
"""Create the Cync config entry using input user data."""
cync_user = self.cync_auth.user
await self.async_set_unique_id(str(cync_user.user_id))
self._abort_if_unique_id_configured()
config = {
CONF_USER_ID: cync_user.user_id,
CONF_AUTHORIZE_STRING: cync_user.authorize,
CONF_EXPIRES_AT: cync_user.expires_at,
CONF_ACCESS_TOKEN: cync_user.access_token,
CONF_REFRESH_TOKEN: cync_user.refresh_token,
}
return self.async_create_entry(title=user_email, data=config)

View File

@@ -1,9 +0,0 @@
"""Constants for the Cync integration."""
DOMAIN = "cync"
CONF_TWO_FACTOR_CODE = "two_factor_code"
CONF_USER_ID = "user_id"
CONF_AUTHORIZE_STRING = "authorize_string"
CONF_EXPIRES_AT = "expires_at"
CONF_REFRESH_TOKEN = "refresh_token"

View File

@@ -1,87 +0,0 @@
"""Coordinator to handle keeping device states up to date."""
from __future__ import annotations
from datetime import timedelta
import logging
import time
from pycync import Cync, CyncDevice, User
from pycync.exceptions import AuthFailedError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_EXPIRES_AT, CONF_REFRESH_TOKEN
_LOGGER = logging.getLogger(__name__)
type CyncConfigEntry = ConfigEntry[CyncCoordinator]
class CyncCoordinator(DataUpdateCoordinator[dict[int, CyncDevice]]):
"""Coordinator to handle updating Cync device states."""
config_entry: CyncConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: CyncConfigEntry, cync: Cync
) -> None:
"""Initialize the Cync coordinator."""
super().__init__(
hass,
_LOGGER,
name="Cync Data Coordinator",
config_entry=config_entry,
update_interval=timedelta(seconds=30),
always_update=True,
)
self.cync = cync
async def on_data_update(self, data: dict[int, CyncDevice]) -> None:
"""Update registered devices with new data."""
merged_data = self.data | data if self.data else data
self.async_set_updated_data(merged_data)
async def _async_setup(self) -> None:
"""Set up the coordinator with initial device states."""
logged_in_user = self.cync.get_logged_in_user()
if logged_in_user.access_token != self.config_entry.data[CONF_ACCESS_TOKEN]:
await self._update_config_cync_credentials(logged_in_user)
async def _async_update_data(self) -> dict[int, CyncDevice]:
"""First, refresh the user's auth token if it is set to expire in less than one hour.
Then, fetch all current device states.
"""
logged_in_user = self.cync.get_logged_in_user()
if logged_in_user.expires_at - time.time() < 3600:
await self._async_refresh_cync_credentials()
self.cync.update_device_states()
current_device_states = self.cync.get_devices()
return {device.device_id: device for device in current_device_states}
async def _async_refresh_cync_credentials(self) -> None:
"""Attempt to refresh the Cync user's authentication token."""
try:
refreshed_user = await self.cync.refresh_credentials()
except AuthFailedError as ex:
raise ConfigEntryAuthFailed("Unable to refresh user token") from ex
else:
await self._update_config_cync_credentials(refreshed_user)
async def _update_config_cync_credentials(self, user_info: User) -> None:
"""Update the config entry with current user info."""
new_data = {**self.config_entry.data}
new_data[CONF_ACCESS_TOKEN] = user_info.access_token
new_data[CONF_REFRESH_TOKEN] = user_info.refresh_token
new_data[CONF_EXPIRES_AT] = user_info.expires_at
self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)

View File

@@ -1,45 +0,0 @@
"""Setup for a generic entity type for the Cync integration."""
from pycync.devices import CyncDevice
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import CyncCoordinator
class CyncBaseEntity(CoordinatorEntity[CyncCoordinator]):
"""Generic base entity for Cync devices."""
_attr_has_entity_name = True
def __init__(
self,
device: CyncDevice,
coordinator: CyncCoordinator,
room_name: str | None = None,
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._cync_device_id = device.device_id
self._attr_unique_id = device.unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.unique_id)},
manufacturer="GE Lighting",
name=device.name,
suggested_area=room_name,
)
@property
def available(self) -> bool:
"""Determines whether this device is currently available."""
return (
super().available
and self.coordinator.data is not None
and self._cync_device_id in self.coordinator.data
and self.coordinator.data[self._cync_device_id].is_online
)

View File

@@ -1,180 +0,0 @@
"""Support for Cync light entities."""
from typing import Any
from pycync import CyncLight
from pycync.devices.capabilities import CyncCapability
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ColorMode,
LightEntity,
filter_supported_color_modes,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.color import value_to_brightness
from homeassistant.util.scaling import scale_ranged_value_to_int_range
from .coordinator import CyncConfigEntry, CyncCoordinator
from .entity import CyncBaseEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: CyncConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Cync lights from a config entry."""
coordinator = entry.runtime_data
cync = coordinator.cync
entities_to_add = []
for home in cync.get_homes():
for room in home.rooms:
room_lights = [
CyncLightEntity(device, coordinator, room.name)
for device in room.devices
if isinstance(device, CyncLight)
]
entities_to_add.extend(room_lights)
group_lights = [
CyncLightEntity(device, coordinator, room.name)
for group in room.groups
for device in group.devices
if isinstance(device, CyncLight)
]
entities_to_add.extend(group_lights)
async_add_entities(entities_to_add)
class CyncLightEntity(CyncBaseEntity, LightEntity):
"""Representation of a Cync light."""
_attr_color_mode = ColorMode.ONOFF
_attr_min_color_temp_kelvin = 2000
_attr_max_color_temp_kelvin = 7000
_attr_translation_key = "light"
_attr_name = None
BRIGHTNESS_SCALE = (0, 100)
def __init__(
self,
device: CyncLight,
coordinator: CyncCoordinator,
room_name: str | None = None,
) -> None:
"""Set up base attributes."""
super().__init__(device, coordinator, room_name)
supported_color_modes = {ColorMode.ONOFF}
if device.supports_capability(CyncCapability.CCT_COLOR):
supported_color_modes.add(ColorMode.COLOR_TEMP)
if device.supports_capability(CyncCapability.DIMMING):
supported_color_modes.add(ColorMode.BRIGHTNESS)
if device.supports_capability(CyncCapability.RGB_COLOR):
supported_color_modes.add(ColorMode.RGB)
self._attr_supported_color_modes = filter_supported_color_modes(
supported_color_modes
)
@property
def is_on(self) -> bool | None:
"""Return True if the light is on."""
return self._device.is_on
@property
def brightness(self) -> int:
"""Provide the light's current brightness."""
return value_to_brightness(self.BRIGHTNESS_SCALE, self._device.brightness)
@property
def color_temp_kelvin(self) -> int:
"""Return color temperature in kelvin."""
return scale_ranged_value_to_int_range(
(1, 100),
(self.min_color_temp_kelvin, self.max_color_temp_kelvin),
self._device.color_temp,
)
@property
def rgb_color(self) -> tuple[int, int, int]:
"""Provide the light's current color in RGB format."""
return self._device.rgb
@property
def color_mode(self) -> str | None:
"""Return the active color mode."""
if (
self._device.supports_capability(CyncCapability.CCT_COLOR)
and self._device.color_mode > 0
and self._device.color_mode <= 100
):
return ColorMode.COLOR_TEMP
if (
self._device.supports_capability(CyncCapability.RGB_COLOR)
and self._device.color_mode == 254
):
return ColorMode.RGB
if self._device.supports_capability(CyncCapability.DIMMING):
return ColorMode.BRIGHTNESS
return ColorMode.ONOFF
async def async_turn_on(self, **kwargs: Any) -> None:
"""Process an action on the light."""
if not kwargs:
await self._device.turn_on()
elif kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None:
color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
converted_color_temp = self._normalize_color_temp(color_temp)
await self._device.set_color_temp(converted_color_temp)
elif kwargs.get(ATTR_RGB_COLOR) is not None:
rgb = kwargs.get(ATTR_RGB_COLOR)
await self._device.set_rgb(rgb)
elif kwargs.get(ATTR_BRIGHTNESS) is not None:
brightness = kwargs.get(ATTR_BRIGHTNESS)
converted_brightness = self._normalize_brightness(brightness)
await self._device.set_brightness(converted_brightness)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self._device.turn_off()
def _normalize_brightness(self, brightness: float | None) -> int | None:
"""Return calculated brightness value scaled between 0-100."""
if brightness is not None:
return int((brightness / 255) * 100)
return None
def _normalize_color_temp(self, color_temp_kelvin: float | None) -> int | None:
"""Return calculated color temp value scaled between 1-100."""
if color_temp_kelvin is not None:
kelvin_range = self.max_color_temp_kelvin - self.min_color_temp_kelvin
scaled_kelvin = int(
((color_temp_kelvin - self.min_color_temp_kelvin) / kelvin_range) * 100
)
if scaled_kelvin == 0:
scaled_kelvin += 1
return scaled_kelvin
return None
@property
def _device(self) -> CyncLight:
"""Fetch the reference to the backing Cync light for this device."""
return self.coordinator.data[self._cync_device_id]

View File

@@ -1,11 +0,0 @@
{
"domain": "cync",
"name": "Cync",
"codeowners": ["@Kinachi249"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cync",
"integration_type": "hub",
"iot_class": "cloud_push",
"quality_scale": "bronze",
"requirements": ["pycync==0.4.0"]
}

View File

@@ -1,69 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: todo
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -1,32 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "Your Cync account's email address",
"password": "Your Cync account's password"
}
},
"two_factor": {
"data": {
"two_factor_code": "Two-factor code"
},
"data_description": {
"two_factor_code": "The two-factor code sent to your Cync account's email"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
}

View File

@@ -37,7 +37,7 @@
}
},
"progress": {
"check_deletion_status": "Please open the {ekeybionyx} app and confirm the deletion of the functions."
"check_deletion_status": "Please go to the {ekeybionyx} app and confirm the deletion of the functions."
},
"error": {
"invalid_name": "Name is invalid",
@@ -55,7 +55,7 @@
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"no_available_webhooks": "There are no available webhooks in the {ekeybionyx} system. Please delete some and try again.",
"no_available_webhooks": "There are no available webhooks in the {ekeybionyx} plattform. Please delete some and try again.",
"no_own_systems": "Your account does not have admin access to any systems.",
"cannot_connect": "Connection to {ekeybionyx} failed. Please check your Internet connection and try again."
},

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250924.0"]
"requirements": ["home-assistant-frontend==20250903.5"]
}

View File

@@ -28,7 +28,7 @@ from homeassistant.config_entries import (
OptionsFlow,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, progress_step
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.hassio import is_hassio
@@ -72,6 +72,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
"""Base flow to install firmware."""
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
_failed_addon_name: str
_failed_addon_reason: str
_picked_firmware_type: PickedFirmwareType
def __init__(self, *args: Any, **kwargs: Any) -> None:
@@ -83,6 +85,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
self._hardware_name: str = "unknown" # To be set in a subclass
self._zigbee_integration = ZigbeeIntegration.ZHA
self.addon_install_task: asyncio.Task | None = None
self.addon_start_task: asyncio.Task | None = None
self.addon_uninstall_task: asyncio.Task | None = None
self.firmware_install_task: asyncio.Task[None] | None = None
self.installing_firmware_name: str | None = None
@@ -123,12 +127,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
) -> ConfigFlowResult:
"""Pick Thread or Zigbee firmware."""
# Determine if ZHA or Thread are already configured to present migrate options
zha_entries = self.hass.config_entries.async_entries(
ZHA_DOMAIN, include_ignore=False
)
otbr_entries = self.hass.config_entries.async_entries(
OTBR_DOMAIN, include_ignore=False
)
zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
otbr_entries = self.hass.config_entries.async_entries(OTBR_DOMAIN)
return self.async_show_menu(
step_id="pick_firmware",
@@ -486,6 +486,18 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
"""Install Zigbee firmware."""
raise NotImplementedError
async def async_step_addon_operation_failed(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Abort when add-on installation or start failed."""
return self.async_abort(
reason=self._failed_addon_reason,
description_placeholders={
**self._get_translation_placeholders(),
"addon_name": self._failed_addon_name,
},
)
async def async_step_pre_confirm_zigbee(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -549,12 +561,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
"""Install Thread firmware."""
raise NotImplementedError
@progress_step(
description_placeholders=lambda self: {
**self._get_translation_placeholders(),
"addon_name": get_otbr_addon_manager(self.hass).addon_name,
}
)
async def async_step_install_otbr_addon(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -564,43 +570,70 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
_LOGGER.debug("OTBR addon info: %s", addon_info)
try:
await addon_manager.async_install_addon_waiting()
except AddonError as err:
_LOGGER.error(err)
raise AbortFlow(
"addon_install_failed",
if not self.addon_install_task:
self.addon_install_task = self.hass.async_create_task(
addon_manager.async_install_addon_waiting(),
"OTBR addon install",
)
if not self.addon_install_task.done():
return self.async_show_progress(
step_id="install_otbr_addon",
progress_action="install_addon",
description_placeholders={
**self._get_translation_placeholders(),
"addon_name": addon_manager.addon_name,
},
) from err
progress_task=self.addon_install_task,
)
return await self.async_step_finish_thread_installation()
try:
await self.addon_install_task
except AddonError as err:
_LOGGER.error(err)
self._failed_addon_name = addon_manager.addon_name
self._failed_addon_reason = "addon_install_failed"
return self.async_show_progress_done(next_step_id="addon_operation_failed")
finally:
self.addon_install_task = None
return self.async_show_progress_done(next_step_id="finish_thread_installation")
@progress_step(
description_placeholders=lambda self: {
**self._get_translation_placeholders(),
"addon_name": get_otbr_addon_manager(self.hass).addon_name,
}
)
async def async_step_start_otbr_addon(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Configure OTBR to point to the SkyConnect and run the addon."""
try:
await self._configure_and_start_otbr_addon()
except AddonError as err:
_LOGGER.error(err)
raise AbortFlow(
"addon_start_failed",
otbr_manager = get_otbr_addon_manager(self.hass)
if not self.addon_start_task:
self.addon_start_task = self.hass.async_create_task(
self._configure_and_start_otbr_addon()
)
if not self.addon_start_task.done():
return self.async_show_progress(
step_id="start_otbr_addon",
progress_action="start_otbr_addon",
description_placeholders={
**self._get_translation_placeholders(),
"addon_name": get_otbr_addon_manager(self.hass).addon_name,
"addon_name": otbr_manager.addon_name,
},
) from err
progress_task=self.addon_start_task,
)
return await self.async_step_pre_confirm_otbr()
try:
await self.addon_start_task
except (AddonError, AbortFlow) as err:
_LOGGER.error(err)
self._failed_addon_name = otbr_manager.addon_name
self._failed_addon_reason = (
err.reason if isinstance(err, AbortFlow) else "addon_start_failed"
)
return self.async_show_progress_done(next_step_id="addon_operation_failed")
finally:
self.addon_start_task = None
return self.async_show_progress_done(next_step_id="pre_confirm_otbr")
async def async_step_pre_confirm_otbr(
self, user_input: dict[str, Any] | None = None

View File

@@ -63,7 +63,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller:
controller = Controller(
async_zeroconf_instance=async_zeroconf_instance,
bleak_scanner_instance=bleak_scanner_instance,
bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type]
char_cache=char_cache,
)

View File

@@ -0,0 +1,28 @@
"""Analytics platform."""
from homeassistant.components.analytics import (
AnalyticsInput,
AnalyticsModifications,
EntityAnalyticsModifications,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
async def async_modify_analytics(
hass: HomeAssistant, analytics_input: AnalyticsInput
) -> AnalyticsModifications:
"""Modify the analytics."""
ent_reg = er.async_get(hass)
entities: dict[str, EntityAnalyticsModifications] = {}
for entity_id in analytics_input.entity_ids:
entity_entry = ent_reg.entities[entity_id]
if entity_entry.capabilities is not None:
capabilities = dict(entity_entry.capabilities)
capabilities["options"] = len(capabilities["options"])
entities[entity_id] = EntityAnalyticsModifications(
capabilities=capabilities
)
return AnalyticsModifications(entities=entities)

View File

@@ -25,7 +25,6 @@ from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,

View File

@@ -20,14 +20,6 @@
}
}
},
"number": {
"light_brightness": {
"default": "mdi:brightness-5"
},
"plant_days": {
"default": "mdi:calendar-blank"
}
},
"select": {
"display_temperature_unit": {
"default": "mdi:thermometer-lines"

View File

@@ -1,136 +0,0 @@
"""Support for LetPot number entities."""
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from letpot.deviceclient import LetPotDeviceClient
from letpot.models import DeviceFeature
from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PRECISION_WHOLE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
from .entity import LetPotEntity, LetPotEntityDescription, exception_handler
# Each change pushes a 'full' device status with the change. The library will cache
# pending changes to avoid overwriting, but try to avoid a lot of parallelism.
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class LetPotNumberEntityDescription(LetPotEntityDescription, NumberEntityDescription):
"""Describes a LetPot number entity."""
max_value_fn: Callable[[LetPotDeviceCoordinator], float]
value_fn: Callable[[LetPotDeviceCoordinator], float | None]
set_value_fn: Callable[[LetPotDeviceClient, str, float], Coroutine[Any, Any, None]]
NUMBERS: tuple[LetPotNumberEntityDescription, ...] = (
LetPotNumberEntityDescription(
key="light_brightness_levels",
translation_key="light_brightness",
value_fn=(
lambda coordinator: coordinator.device_client.get_light_brightness_levels(
coordinator.device.serial_number
).index(coordinator.data.light_brightness)
+ 1
if coordinator.data.light_brightness is not None
else None
),
set_value_fn=(
lambda device_client, serial, value: device_client.set_light_brightness(
serial,
device_client.get_light_brightness_levels(serial)[int(value) - 1],
)
),
supported_fn=(
lambda coordinator: DeviceFeature.LIGHT_BRIGHTNESS_LEVELS
in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
),
native_min_value=float(1),
max_value_fn=lambda coordinator: float(
len(
coordinator.device_client.get_light_brightness_levels(
coordinator.device.serial_number
)
)
),
native_step=PRECISION_WHOLE,
mode=NumberMode.SLIDER,
entity_category=EntityCategory.CONFIG,
),
LetPotNumberEntityDescription(
key="plant_days",
translation_key="plant_days",
value_fn=lambda coordinator: coordinator.data.plant_days,
set_value_fn=(
lambda device_client, serial, value: device_client.set_plant_days(
serial, int(value)
)
),
native_min_value=float(0),
max_value_fn=lambda _: float(999),
native_step=PRECISION_WHOLE,
mode=NumberMode.BOX,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: LetPotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LetPot number entities based on a config entry and device status/features."""
coordinators = entry.runtime_data
async_add_entities(
LetPotNumberEntity(coordinator, description)
for description in NUMBERS
for coordinator in coordinators
if description.supported_fn(coordinator)
)
class LetPotNumberEntity(LetPotEntity, NumberEntity):
"""Defines a LetPot number entity."""
entity_description: LetPotNumberEntityDescription
def __init__(
self,
coordinator: LetPotDeviceCoordinator,
description: LetPotNumberEntityDescription,
) -> None:
"""Initialize LetPot number entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}"
@property
def native_max_value(self) -> float:
"""Return the maximum available value."""
return self.entity_description.max_value_fn(self.coordinator)
@property
def native_value(self) -> float | None:
"""Return the number value."""
return self.entity_description.value_fn(self.coordinator)
@exception_handler
async def async_set_native_value(self, value: float) -> None:
"""Change the number value."""
return await self.entity_description.set_value_fn(
self.coordinator.device_client,
self.coordinator.device.serial_number,
value,
)

View File

@@ -49,15 +49,6 @@
"name": "Refill error"
}
},
"number": {
"light_brightness": {
"name": "Light brightness"
},
"plant_days": {
"name": "Plants age",
"unit_of_measurement": "days"
}
},
"select": {
"display_temperature_unit": {
"name": "Temperature unit on display",
@@ -67,7 +58,7 @@
}
},
"light_brightness": {
"name": "[%key:component::letpot::entity::number::light_brightness::name%]",
"name": "Light brightness",
"state": {
"low": "[%key:common::state::low%]",
"high": "[%key:common::state::high%]"

View File

@@ -117,7 +117,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
conv_brightness = self._convert_brightness_to_modbus(brightness)
await self._hub.async_pb_call(
unit=self._device_address,
device_address=self._device_address,
address=self._brightness_address,
value=conv_brightness,
use_call=CALL_TYPE_WRITE_REGISTER,
@@ -133,7 +133,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
conv_color_temp_kelvin = self._convert_color_temp_to_modbus(color_temp_kelvin)
await self._hub.async_pb_call(
unit=self._device_address,
device_address=self._device_address,
address=self._color_temp_address,
value=conv_color_temp_kelvin,
use_call=CALL_TYPE_WRITE_REGISTER,
@@ -150,7 +150,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
if self._brightness_address:
brightness_result = await self._hub.async_pb_call(
unit=self._device_address,
device_address=self._device_address,
value=1,
address=self._brightness_address,
use_call=CALL_TYPE_REGISTER_HOLDING,
@@ -167,7 +167,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
if self._color_temp_address:
color_result = await self._hub.async_pb_call(
unit=self._device_address,
device_address=self._device_address,
value=1,
address=self._color_temp_address,
use_call=CALL_TYPE_REGISTER_HOLDING,

View File

@@ -370,11 +370,17 @@ class ModbusHub:
_LOGGER.info(f"modbus {self.name} communication closed")
async def low_level_pb_call(
self, slave: int | None, address: int, value: int | list[int], use_call: str
self,
device_address: int | None,
address: int,
value: int | list[int],
use_call: str,
) -> ModbusPDU | None:
"""Call sync. pymodbus."""
kwargs: dict[str, Any] = (
{DEVICE_ID: slave} if slave is not None else {DEVICE_ID: 1}
{DEVICE_ID: device_address}
if device_address is not None
else {DEVICE_ID: 1}
)
entry = self._pb_request[use_call]
@@ -386,28 +392,26 @@ class ModbusHub:
try:
result: ModbusPDU = await entry.func(address, **kwargs)
except ModbusException as exception_error:
error = f"Error: device: {slave} address: {address} -> {exception_error!s}"
error = f"Error: device: {device_address} address: {address} -> {exception_error!s}"
self._log_error(error)
return None
if not result:
error = (
f"Error: device: {slave} address: {address} -> pymodbus returned None"
)
error = f"Error: device: {device_address} address: {address} -> pymodbus returned None"
self._log_error(error)
return None
if not hasattr(result, entry.attr):
error = f"Error: device: {slave} address: {address} -> {result!s}"
error = f"Error: device: {device_address} address: {address} -> {result!s}"
self._log_error(error)
return None
if result.isError():
error = f"Error: device: {slave} address: {address} -> pymodbus returned isError True"
error = f"Error: device: {device_address} address: {address} -> pymodbus returned isError True"
self._log_error(error)
return None
return result
async def async_pb_call(
self,
unit: int | None,
device_address: int | None,
address: int,
value: int | list[int],
use_call: str,
@@ -415,7 +419,7 @@ class ModbusHub:
"""Convert async to sync pymodbus call."""
if not self._client:
return None
result = await self.low_level_pb_call(unit, address, value, use_call)
result = await self.low_level_pb_call(device_address, address, value, use_call)
if self._msg_wait:
await asyncio.sleep(self._msg_wait)
return result

View File

@@ -38,10 +38,7 @@ from homeassistant.core import (
get_hassjob_callable_job_type,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.importlib import async_import_module
from homeassistant.helpers.start import async_at_started
from homeassistant.helpers.typing import ConfigType
@@ -74,7 +71,6 @@ from .const import (
DEFAULT_WS_PATH,
DOMAIN,
MQTT_CONNECTION_STATE,
MQTT_PROCESSED_SUBSCRIPTIONS,
PROTOCOL_5,
PROTOCOL_31,
TRANSPORT_WEBSOCKETS,
@@ -113,7 +109,6 @@ INITIAL_SUBSCRIBE_COOLDOWN = 0.5
SUBSCRIBE_COOLDOWN = 0.1
UNSUBSCRIBE_COOLDOWN = 0.1
TIMEOUT_ACK = 10
SUBSCRIBE_TIMEOUT = 10
RECONNECT_INTERVAL_SECONDS = 10
MAX_WILDCARD_SUBSCRIBES_PER_CALL = 1
@@ -196,47 +191,11 @@ async def async_subscribe(
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
qos: int = DEFAULT_QOS,
encoding: str | None = DEFAULT_ENCODING,
wait: bool = False,
) -> CALLBACK_TYPE:
"""Subscribe to an MQTT topic.
Call the return value to unsubscribe.
"""
subscription_complete: asyncio.Future[None]
async def _sync_mqtt_subscribe(subscriptions: list[tuple[str, int]]) -> None:
if (topic, qos) not in subscriptions:
return
subscription_complete.set_result(None)
def _async_timeout_subscribe() -> None:
if not subscription_complete.done():
subscription_complete.set_exception(TimeoutError)
if (
wait
and DATA_MQTT in hass.data
and not hass.data[DATA_MQTT].client._matching_subscriptions(topic) # noqa: SLF001
):
subscription_complete = hass.loop.create_future()
dispatcher = async_dispatcher_connect(
hass, MQTT_PROCESSED_SUBSCRIPTIONS, _sync_mqtt_subscribe
)
subscribe_callback = async_subscribe_internal(
hass, topic, msg_callback, qos, encoding
)
try:
hass.loop.call_later(SUBSCRIBE_TIMEOUT, _async_timeout_subscribe)
await subscription_complete
except TimeoutError as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="subscribe_timeout",
) from exc
finally:
dispatcher()
return subscribe_callback
return async_subscribe_internal(hass, topic, msg_callback, qos, encoding)
@@ -1004,7 +963,6 @@ class MQTT:
self._last_subscribe = time.monotonic()
await self._async_wait_for_mid_or_raise(mid, result)
async_dispatcher_send(self.hass, MQTT_PROCESSED_SUBSCRIPTIONS, chunk_list)
async def _async_perform_unsubscribes(self) -> None:
"""Perform pending MQTT client unsubscribes."""

View File

@@ -370,7 +370,6 @@ DOMAIN = "mqtt"
LOGGER = logging.getLogger(__package__)
MQTT_CONNECTION_STATE = "mqtt_connection_state"
MQTT_PROCESSED_SUBSCRIPTIONS = "mqtt_processed_subscriptions"
PAYLOAD_EMPTY_JSON = "{}"
PAYLOAD_NONE = "None"

View File

@@ -6,7 +6,6 @@
"config_flow": true,
"dependencies": ["file_upload", "http"],
"documentation": "https://www.home-assistant.io/integrations/mqtt",
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["paho-mqtt==2.1.0"],

View File

@@ -1 +0,0 @@
"""Neo virtual integration."""

View File

@@ -1,6 +0,0 @@
{
"domain": "neo",
"name": "Neo",
"integration_type": "virtual",
"supported_by": "shelly"
}

View File

@@ -473,12 +473,7 @@ class TopicSubentryFlowHandler(ConfigSubentryFlow):
return self.async_update_and_abort(
entry=entry,
subentry=subentry,
data_updates={
CONF_PRIORITY: user_input.get(CONF_PRIORITY),
CONF_TAGS: user_input.get(CONF_TAGS),
CONF_TITLE: user_input.get(CONF_TITLE),
CONF_MESSAGE: user_input.get(CONF_MESSAGE),
},
data_updates=user_input,
)
return self.async_show_form(

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "silver",
"requirements": ["renault-api==0.4.1"]
"requirements": ["renault-api==0.4.0"]
}

View File

@@ -243,45 +243,8 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
await super().async_will_remove_from_hass()
class ReolinkHostChimeCoordinatorEntity(ReolinkHostCoordinatorEntity):
"""Parent class for Reolink chime entities connected to a Host."""
def __init__(
self,
reolink_data: ReolinkData,
chime: Chime,
coordinator: DataUpdateCoordinator[None] | None = None,
) -> None:
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
super().__init__(reolink_data, coordinator)
self._channel = chime.channel
self._chime = chime
self._attr_unique_id = (
f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}"
)
via_dev_id = self._host.unique_id
self._dev_id = f"{self._host.unique_id}_chime{chime.dev_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._dev_id)},
via_device=(DOMAIN, via_dev_id),
name=chime.name,
model="Reolink Chime",
manufacturer=self._host.api.manufacturer,
sw_version=chime.sw_version,
serial_number=str(chime.dev_id),
configuration_url=self._conf_url,
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self._chime.online
class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
"""Parent class for Reolink chime entities connected through a camera."""
"""Parent class for Reolink chime entities connected."""
def __init__(
self,
@@ -292,21 +255,21 @@ class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
assert chime.channel is not None
super().__init__(reolink_data, chime.channel, coordinator)
self._chime = chime
self._attr_unique_id = (
f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}"
)
via_dev_id = self._dev_id
cam_dev_id = self._dev_id
self._dev_id = f"{self._host.unique_id}_chime{chime.dev_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._dev_id)},
via_device=(DOMAIN, via_dev_id),
via_device=(DOMAIN, cam_dev_id),
name=chime.name,
model="Reolink Chime",
manufacturer=self._host.api.manufacturer,
sw_version=chime.sw_version,
serial_number=str(chime.dev_id),
configuration_url=self._conf_url,
)

View File

@@ -23,7 +23,6 @@ from .entity import (
ReolinkChannelEntityDescription,
ReolinkChimeCoordinatorEntity,
ReolinkChimeEntityDescription,
ReolinkHostChimeCoordinatorEntity,
ReolinkHostCoordinatorEntity,
ReolinkHostEntityDescription,
)
@@ -856,12 +855,6 @@ async def async_setup_entry(
for chime in api.chime_list
if chime.channel is not None
)
entities.extend(
ReolinkHostChimeNumberEntity(reolink_data, chime, entity_description)
for entity_description in CHIME_NUMBER_ENTITIES
for chime in api.chime_list
if chime.channel is None
)
async_add_entities(entities)
@@ -976,36 +969,7 @@ class ReolinkHostNumberEntity(ReolinkHostCoordinatorEntity, NumberEntity):
class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity):
"""Base number entity class for Reolink chimes connected through a camera."""
entity_description: ReolinkChimeNumberEntityDescription
def __init__(
self,
reolink_data: ReolinkData,
chime: Chime,
entity_description: ReolinkChimeNumberEntityDescription,
) -> None:
"""Initialize Reolink chime number entity."""
self.entity_description = entity_description
super().__init__(reolink_data, chime)
self._attr_mode = entity_description.mode
@property
def native_value(self) -> float | None:
"""State of the number entity."""
return self.entity_description.value(self._chime)
@raise_translated_error
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await self.entity_description.method(self._chime, value)
self.async_write_ha_state()
class ReolinkHostChimeNumberEntity(ReolinkHostChimeCoordinatorEntity, NumberEntity):
"""Base number entity class for Reolink chimes connected to the host."""
"""Base number entity class for Reolink IP cameras."""
entity_description: ReolinkChimeNumberEntityDescription

View File

@@ -31,7 +31,6 @@ from .entity import (
ReolinkChannelEntityDescription,
ReolinkChimeCoordinatorEntity,
ReolinkChimeEntityDescription,
ReolinkHostChimeCoordinatorEntity,
ReolinkHostCoordinatorEntity,
ReolinkHostEntityDescription,
)
@@ -74,7 +73,7 @@ class ReolinkChimeSelectEntityDescription(
get_options: list[str]
method: Callable[[Chime, str], Any]
value: Callable[[Chime], str | None]
value: Callable[[Chime], str]
def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int:
@@ -333,7 +332,7 @@ CHIME_SELECT_ENTITIES = (
entity_category=EntityCategory.CONFIG,
supported=lambda chime: "md" in chime.chime_event_types,
get_options=[method.name for method in ChimeToneEnum],
value=lambda chime: chime.tone_name("md"),
value=lambda chime: ChimeToneEnum(chime.tone("md")).name,
method=lambda chime, name: chime.set_tone("md", ChimeToneEnum[name].value),
),
ReolinkChimeSelectEntityDescription(
@@ -343,7 +342,7 @@ CHIME_SELECT_ENTITIES = (
entity_category=EntityCategory.CONFIG,
get_options=[method.name for method in ChimeToneEnum],
supported=lambda chime: "people" in chime.chime_event_types,
value=lambda chime: chime.tone_name("people"),
value=lambda chime: ChimeToneEnum(chime.tone("people")).name,
method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value),
),
ReolinkChimeSelectEntityDescription(
@@ -353,7 +352,7 @@ CHIME_SELECT_ENTITIES = (
entity_category=EntityCategory.CONFIG,
get_options=[method.name for method in ChimeToneEnum],
supported=lambda chime: "vehicle" in chime.chime_event_types,
value=lambda chime: chime.tone_name("vehicle"),
value=lambda chime: ChimeToneEnum(chime.tone("vehicle")).name,
method=lambda chime, name: chime.set_tone("vehicle", ChimeToneEnum[name].value),
),
ReolinkChimeSelectEntityDescription(
@@ -363,7 +362,7 @@ CHIME_SELECT_ENTITIES = (
entity_category=EntityCategory.CONFIG,
get_options=[method.name for method in ChimeToneEnum],
supported=lambda chime: "visitor" in chime.chime_event_types,
value=lambda chime: chime.tone_name("visitor"),
value=lambda chime: ChimeToneEnum(chime.tone("visitor")).name,
method=lambda chime, name: chime.set_tone("visitor", ChimeToneEnum[name].value),
),
ReolinkChimeSelectEntityDescription(
@@ -373,7 +372,7 @@ CHIME_SELECT_ENTITIES = (
entity_category=EntityCategory.CONFIG,
get_options=[method.name for method in ChimeToneEnum],
supported=lambda chime: "package" in chime.chime_event_types,
value=lambda chime: chime.tone_name("package"),
value=lambda chime: ChimeToneEnum(chime.tone("package")).name,
method=lambda chime, name: chime.set_tone("package", ChimeToneEnum[name].value),
),
)
@@ -387,7 +386,9 @@ async def async_setup_entry(
"""Set up a Reolink select entities."""
reolink_data: ReolinkData = config_entry.runtime_data
entities: list[SelectEntity] = [
entities: list[
ReolinkSelectEntity | ReolinkHostSelectEntity | ReolinkChimeSelectEntity
] = [
ReolinkSelectEntity(reolink_data, channel, entity_description)
for entity_description in SELECT_ENTITIES
for channel in reolink_data.host.api.channels
@@ -404,12 +405,6 @@ async def async_setup_entry(
for chime in reolink_data.host.api.chime_list
if entity_description.supported(chime) and chime.channel is not None
)
entities.extend(
ReolinkHostChimeSelectEntity(reolink_data, chime, entity_description)
for entity_description in CHIME_SELECT_ENTITIES
for chime in reolink_data.host.api.chime_list
if entity_description.supported(chime) and chime.channel is None
)
async_add_entities(entities)
@@ -486,7 +481,7 @@ class ReolinkHostSelectEntity(ReolinkHostCoordinatorEntity, SelectEntity):
class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
"""Base select entity class for Reolink chimes connected through a camera."""
"""Base select entity class for Reolink IP cameras."""
entity_description: ReolinkChimeSelectEntityDescription
@@ -499,40 +494,22 @@ class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
"""Initialize Reolink select entity for a chime."""
self.entity_description = entity_description
super().__init__(reolink_data, chime)
self._log_error = True
self._attr_options = entity_description.get_options
@property
def current_option(self) -> str | None:
"""Return the current option."""
return self.entity_description.value(self._chime)
@raise_translated_error
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.method(self._chime, option)
self.async_write_ha_state()
class ReolinkHostChimeSelectEntity(ReolinkHostChimeCoordinatorEntity, SelectEntity):
"""Base select entity class for Reolink chimes connected to a host."""
entity_description: ReolinkChimeSelectEntityDescription
def __init__(
self,
reolink_data: ReolinkData,
chime: Chime,
entity_description: ReolinkChimeSelectEntityDescription,
) -> None:
"""Initialize Reolink select entity for a chime."""
self.entity_description = entity_description
super().__init__(reolink_data, chime)
self._attr_options = entity_description.get_options
@property
def current_option(self) -> str | None:
"""Return the current option."""
return self.entity_description.value(self._chime)
try:
option = self.entity_description.value(self._chime)
except (ValueError, KeyError):
if self._log_error:
_LOGGER.exception("Reolink '%s' has an unknown value", self.name)
self._log_error = False
return None
self._log_error = True
return option
@raise_translated_error
async def async_select_option(self, option: str) -> None:

View File

@@ -20,7 +20,6 @@ from .entity import (
ReolinkChannelEntityDescription,
ReolinkChimeCoordinatorEntity,
ReolinkChimeEntityDescription,
ReolinkHostChimeCoordinatorEntity,
ReolinkHostCoordinatorEntity,
ReolinkHostEntityDescription,
)
@@ -365,7 +364,9 @@ async def async_setup_entry(
"""Set up a Reolink switch entities."""
reolink_data: ReolinkData = config_entry.runtime_data
entities: list[SwitchEntity] = [
entities: list[
ReolinkSwitchEntity | ReolinkNVRSwitchEntity | ReolinkChimeSwitchEntity
] = [
ReolinkSwitchEntity(reolink_data, channel, entity_description)
for entity_description in SWITCH_ENTITIES
for channel in reolink_data.host.api.channels
@@ -382,12 +383,6 @@ async def async_setup_entry(
for chime in reolink_data.host.api.chime_list
if chime.channel is not None
)
entities.extend(
ReolinkHostChimeSwitchEntity(reolink_data, chime, entity_description)
for entity_description in CHIME_SWITCH_ENTITIES
for chime in reolink_data.host.api.chime_list
if chime.channel is None
)
# Can be removed in HA 2025.4.0
depricated_dict = {}
@@ -516,36 +511,3 @@ class ReolinkChimeSwitchEntity(ReolinkChimeCoordinatorEntity, SwitchEntity):
"""Turn the entity off."""
await self.entity_description.method(self._chime, False)
self.async_write_ha_state()
class ReolinkHostChimeSwitchEntity(ReolinkHostChimeCoordinatorEntity, SwitchEntity):
"""Base switch entity class for a chime."""
entity_description: ReolinkChimeSwitchEntityDescription
def __init__(
self,
reolink_data: ReolinkData,
chime: Chime,
entity_description: ReolinkChimeSwitchEntityDescription,
) -> None:
"""Initialize Reolink switch entity."""
self.entity_description = entity_description
super().__init__(reolink_data, chime)
@property
def is_on(self) -> bool | None:
"""Return true if switch is on."""
return self.entity_description.value(self._chime)
@raise_translated_error
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
await self.entity_description.method(self._chime, True)
self.async_write_ha_state()
@raise_translated_error
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
await self.entity_description.method(self._chime, False)
self.async_write_ha_state()

View File

@@ -1,28 +0,0 @@
"""The Smart Meter B Route integration."""
import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import BRouteConfigEntry, BRouteUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: BRouteConfigEntry) -> bool:
"""Set up Smart Meter B Route from a config entry."""
coordinator = BRouteUpdateCoordinator(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: BRouteConfigEntry) -> bool:
"""Unload a config entry."""
await hass.async_add_executor_job(entry.runtime_data.api.close)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,116 +0,0 @@
"""Config flow for Smart Meter B Route integration."""
import logging
from typing import Any
from momonga import Momonga, MomongaSkJoinFailure, MomongaSkScanFailure
from serial.tools.list_ports import comports
from serial.tools.list_ports_common import ListPortInfo
import voluptuous as vol
from homeassistant.components.usb import get_serial_by_id, human_readable_device_name
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_PASSWORD
from homeassistant.core import callback
from homeassistant.helpers.service_info.usb import UsbServiceInfo
from .const import DOMAIN, ENTRY_TITLE
_LOGGER = logging.getLogger(__name__)
def _validate_input(device: str, id: str, password: str) -> None:
"""Validate the user input allows us to connect."""
with Momonga(dev=device, rbid=id, pwd=password):
pass
def _human_readable_device_name(port: UsbServiceInfo | ListPortInfo) -> str:
return human_readable_device_name(
port.device,
port.serial_number,
port.manufacturer,
port.description,
str(port.vid) if port.vid else None,
str(port.pid) if port.pid else None,
)
class BRouteConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Smart Meter B Route."""
VERSION = 1
device: UsbServiceInfo | None = None
@callback
def _get_discovered_device_id_and_name(
self, device_options: dict[str, ListPortInfo]
) -> tuple[str | None, str | None]:
discovered_device_id = (
get_serial_by_id(self.device.device) if self.device else None
)
discovered_device = (
device_options.get(discovered_device_id) if discovered_device_id else None
)
discovered_device_name = (
_human_readable_device_name(discovered_device)
if discovered_device
else None
)
return discovered_device_id, discovered_device_name
async def _get_usb_devices(self) -> dict[str, ListPortInfo]:
"""Return a list of available USB devices."""
devices = await self.hass.async_add_executor_job(comports)
return {get_serial_by_id(port.device): port for port in devices}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
device_options = await self._get_usb_devices()
if user_input is not None:
try:
await self.hass.async_add_executor_job(
_validate_input,
user_input[CONF_DEVICE],
user_input[CONF_ID],
user_input[CONF_PASSWORD],
)
except MomongaSkScanFailure:
errors["base"] = "cannot_connect"
except MomongaSkJoinFailure:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
user_input[CONF_ID], raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=ENTRY_TITLE, data=user_input)
discovered_device_id, discovered_device_name = (
self._get_discovered_device_id_and_name(device_options)
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_DEVICE, default=discovered_device_id): vol.In(
{discovered_device_id: discovered_device_name}
if discovered_device_id and discovered_device_name
else {
name: _human_readable_device_name(device)
for name, device in device_options.items()
}
),
vol.Required(CONF_ID): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
)

View File

@@ -1,12 +0,0 @@
"""Constants for the Smart Meter B Route integration."""
from datetime import timedelta
DOMAIN = "route_b_smart_meter"
ENTRY_TITLE = "Route B Smart Meter"
DEFAULT_SCAN_INTERVAL = timedelta(seconds=300)
ATTR_API_INSTANTANEOUS_POWER = "instantaneous_power"
ATTR_API_TOTAL_CONSUMPTION = "total_consumption"
ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE = "instantaneous_current_t_phase"
ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE = "instantaneous_current_r_phase"

View File

@@ -1,75 +0,0 @@
"""DataUpdateCoordinator for the Smart Meter B-route integration."""
from dataclasses import dataclass
import logging
from momonga import Momonga, MomongaError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclass
class BRouteData:
"""Class for data of the B Route."""
instantaneous_current_r_phase: float
instantaneous_current_t_phase: float
instantaneous_power: float
total_consumption: float
type BRouteConfigEntry = ConfigEntry[BRouteUpdateCoordinator]
class BRouteUpdateCoordinator(DataUpdateCoordinator[BRouteData]):
"""The B Route update coordinator."""
def __init__(
self,
hass: HomeAssistant,
entry: BRouteConfigEntry,
) -> None:
"""Initialize."""
self.device = entry.data[CONF_DEVICE]
self.bid = entry.data[CONF_ID]
password = entry.data[CONF_PASSWORD]
self.api = Momonga(dev=self.device, rbid=self.bid, pwd=password)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
config_entry=entry,
update_interval=DEFAULT_SCAN_INTERVAL,
)
async def _async_setup(self) -> None:
await self.hass.async_add_executor_job(
self.api.open,
)
def _get_data(self) -> BRouteData:
"""Get the data from API."""
current = self.api.get_instantaneous_current()
return BRouteData(
instantaneous_current_r_phase=current["r phase current"],
instantaneous_current_t_phase=current["t phase current"],
instantaneous_power=self.api.get_instantaneous_power(),
total_consumption=self.api.get_measured_cumulative_energy(),
)
async def _async_update_data(self) -> BRouteData:
"""Update data."""
try:
return await self.hass.async_add_executor_job(self._get_data)
except MomongaError as error:
raise UpdateFailed(error) from error

View File

@@ -1,17 +0,0 @@
{
"domain": "route_b_smart_meter",
"name": "Smart Meter B Route",
"codeowners": ["@SeraphicRav"],
"config_flow": true,
"dependencies": ["usb"],
"documentation": "https://www.home-assistant.io/integrations/route_b_smart_meter",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": [
"momonga.momonga",
"momonga.momonga_session_manager",
"momonga.sk_wrapper_logger"
],
"quality_scale": "bronze",
"requirements": ["pyserial==3.5", "momonga==0.1.5"]
}

View File

@@ -1,82 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
The integration does not provide any additional actions.
appropriate-polling:
status: done
brands:
status: exempt
comment: |
The integration is not specific to a single brand, it does not have a logo.
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
The integration does not provide any additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
The integration does not use events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
The integration does not provide any additional actions.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: done
discovery:
status: exempt
comment: |
The manufacturer does not use unique identifiers for devices.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession:
status: exempt
comment: |
The integration does not use HTTP.
strict-typing: todo

View File

@@ -1,109 +0,0 @@
"""Smart Meter B Route."""
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfElectricCurrent, UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BRouteConfigEntry
from .const import (
ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE,
ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE,
ATTR_API_INSTANTANEOUS_POWER,
ATTR_API_TOTAL_CONSUMPTION,
DOMAIN,
)
from .coordinator import BRouteData, BRouteUpdateCoordinator
@dataclass(frozen=True, kw_only=True)
class SensorEntityDescriptionWithValueAccessor(SensorEntityDescription):
"""Sensor entity description with data accessor."""
value_accessor: Callable[[BRouteData], StateType]
SENSOR_DESCRIPTIONS = (
SensorEntityDescriptionWithValueAccessor(
key=ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE,
translation_key=ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_accessor=lambda data: data.instantaneous_current_r_phase,
),
SensorEntityDescriptionWithValueAccessor(
key=ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE,
translation_key=ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_accessor=lambda data: data.instantaneous_current_t_phase,
),
SensorEntityDescriptionWithValueAccessor(
key=ATTR_API_INSTANTANEOUS_POWER,
translation_key=ATTR_API_INSTANTANEOUS_POWER,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
value_accessor=lambda data: data.instantaneous_power,
),
SensorEntityDescriptionWithValueAccessor(
key=ATTR_API_TOTAL_CONSUMPTION,
translation_key=ATTR_API_TOTAL_CONSUMPTION,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_accessor=lambda data: data.total_consumption,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: BRouteConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Smart Meter B-route entry."""
coordinator = entry.runtime_data
async_add_entities(
SmartMeterBRouteSensor(coordinator, description)
for description in SENSOR_DESCRIPTIONS
)
class SmartMeterBRouteSensor(CoordinatorEntity[BRouteUpdateCoordinator], SensorEntity):
"""Representation of a Smart Meter B-route sensor entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: BRouteUpdateCoordinator,
description: SensorEntityDescriptionWithValueAccessor,
) -> None:
"""Initialize Smart Meter B-route sensor entity."""
super().__init__(coordinator)
self.entity_description: SensorEntityDescriptionWithValueAccessor = description
self._attr_unique_id = f"{coordinator.bid}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.bid)},
name=f"Route B Smart Meter {coordinator.bid}",
)
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_accessor(self.coordinator.data)

View File

@@ -1,42 +0,0 @@
{
"config": {
"step": {
"user": {
"data_description": {
"device": "[%key:common::config_flow::data::device%]",
"id": "B Route ID",
"password": "[%key:common::config_flow::data::password%]"
},
"data": {
"device": "[%key:common::config_flow::data::device%]",
"id": "B Route ID",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"instantaneous_power": {
"name": "Instantaneous power"
},
"total_consumption": {
"name": "Total consumption"
},
"instantaneous_current_t_phase": {
"name": "Instantaneous current T phase"
},
"instantaneous_current_r_phase": {
"name": "Instantaneous current R phase"
}
}
}
}

View File

@@ -26,7 +26,6 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_ENQUEUE,
ATTR_MEDIA_EXTRA,
ATTR_MEDIA_TITLE,
BrowseMedia,
MediaPlayerDeviceClass,
@@ -539,14 +538,26 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
share_link = self.coordinator.share_link
if share_link.is_share_link(media_id):
title = kwargs.get(ATTR_MEDIA_EXTRA, {}).get("title", "")
self._play_media_sharelink(
soco=soco,
media_type=media_type,
media_id=media_id,
enqueue=enqueue,
title=title,
)
if enqueue == MediaPlayerEnqueue.ADD:
share_link.add_share_link_to_queue(
media_id, timeout=LONG_SERVICE_TIMEOUT
)
elif enqueue in (
MediaPlayerEnqueue.NEXT,
MediaPlayerEnqueue.PLAY,
):
pos = (self.media.queue_position or 0) + 1
new_pos = share_link.add_share_link_to_queue(
media_id, position=pos, timeout=LONG_SERVICE_TIMEOUT
)
if enqueue == MediaPlayerEnqueue.PLAY:
soco.play_from_queue(new_pos - 1)
elif enqueue == MediaPlayerEnqueue.REPLACE:
soco.clear_queue()
share_link.add_share_link_to_queue(
media_id, timeout=LONG_SERVICE_TIMEOUT
)
soco.play_from_queue(0)
elif media_type == MEDIA_TYPE_DIRECTORY:
self._play_media_directory(
soco=soco, media_type=media_type, media_id=media_id, enqueue=enqueue
@@ -652,39 +663,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
)
self._play_media_queue(soco, item, enqueue)
def _play_media_sharelink(
self,
soco: SoCo,
media_type: MediaType | str,
media_id: str,
enqueue: MediaPlayerEnqueue,
title: str,
) -> None:
share_link = self.coordinator.share_link
kwargs = {}
if title:
kwargs["dc_title"] = title
if enqueue == MediaPlayerEnqueue.ADD:
share_link.add_share_link_to_queue(
media_id, timeout=LONG_SERVICE_TIMEOUT, **kwargs
)
elif enqueue in (
MediaPlayerEnqueue.NEXT,
MediaPlayerEnqueue.PLAY,
):
pos = (self.media.queue_position or 0) + 1
new_pos = share_link.add_share_link_to_queue(
media_id, position=pos, timeout=LONG_SERVICE_TIMEOUT, **kwargs
)
if enqueue == MediaPlayerEnqueue.PLAY:
soco.play_from_queue(new_pos - 1)
elif enqueue == MediaPlayerEnqueue.REPLACE:
soco.clear_queue()
share_link.add_share_link_to_queue(
media_id, timeout=LONG_SERVICE_TIMEOUT, **kwargs
)
soco.play_from_queue(0)
@soco_error()
def set_sleep_timer(self, sleep_time: int) -> None:
"""Set the timer on the player."""

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
@@ -48,8 +48,11 @@ TAMPER_BINARY_SENSOR = TuyaBinarySensorEntityDescription(
# All descriptions can be found here. Mostly the Boolean data types in the
# default status set of each category (that don't have a set instruction)
# end up being a binary sensor.
BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ...]] = {
DeviceCategory.CO2BJ: (
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
# CO2 Detector
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
"co2bj": (
TuyaBinarySensorEntityDescription(
key=DPCode.CO2_STATE,
device_class=BinarySensorDeviceClass.SAFETY,
@@ -57,7 +60,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.COBJ: (
# CO Detector
# https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v
"cobj": (
TuyaBinarySensorEntityDescription(
key=DPCode.CO_STATE,
device_class=BinarySensorDeviceClass.SAFETY,
@@ -70,7 +75,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.CS: (
# Dehumidifier
# https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
"cs": (
TuyaBinarySensorEntityDescription(
key="tankfull",
dpcode=DPCode.FAULT,
@@ -96,14 +103,18 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
translation_key="wet",
),
),
DeviceCategory.CWWSQ: (
# Smart Pet Feeder
# https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
"cwwsq": (
TuyaBinarySensorEntityDescription(
key=DPCode.FEED_STATE,
translation_key="feeding",
on_value="feeding",
),
),
DeviceCategory.DGNBJ: (
# Multi-functional Sensor
# https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
"dgnbj": (
TuyaBinarySensorEntityDescription(
key=DPCode.GAS_SENSOR_STATE,
device_class=BinarySensorDeviceClass.GAS,
@@ -166,14 +177,18 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.HPS: (
# Human Presence Sensor
# https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs
"hps": (
TuyaBinarySensorEntityDescription(
key=DPCode.PRESENCE_STATE,
device_class=BinarySensorDeviceClass.OCCUPANCY,
on_value={"presence", "small_move", "large_move", "peaceful"},
),
),
DeviceCategory.JQBJ: (
# Formaldehyde Detector
# Note: Not documented
"jqbj": (
TuyaBinarySensorEntityDescription(
key=DPCode.CH2O_STATE,
device_class=BinarySensorDeviceClass.SAFETY,
@@ -181,7 +196,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.JWBJ: (
# Methane Detector
# https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm
"jwbj": (
TuyaBinarySensorEntityDescription(
key=DPCode.CH4_SENSOR_STATE,
device_class=BinarySensorDeviceClass.GAS,
@@ -189,7 +206,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.LDCG: (
# Luminance Sensor
# https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8
"ldcg": (
TuyaBinarySensorEntityDescription(
key=DPCode.TEMPER_ALARM,
device_class=BinarySensorDeviceClass.TAMPER,
@@ -197,14 +216,18 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.MC: (
# Door and Window Controller
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9
"mc": (
TuyaBinarySensorEntityDescription(
key=DPCode.STATUS,
device_class=BinarySensorDeviceClass.DOOR,
on_value={"open", "opened"},
),
),
DeviceCategory.MCS: (
# Door Window Sensor
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m
"mcs": (
TuyaBinarySensorEntityDescription(
key=DPCode.DOORCONTACT_STATE,
device_class=BinarySensorDeviceClass.DOOR,
@@ -215,14 +238,18 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.MK: (
# Access Control
# https://developer.tuya.com/en/docs/iot/s?id=Kb0o2xhlkxbet
"mk": (
TuyaBinarySensorEntityDescription(
key=DPCode.CLOSED_OPENED_KIT,
device_class=BinarySensorDeviceClass.LOCK,
on_value={"AQAB"},
),
),
DeviceCategory.PIR: (
# PIR Detector
# https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80
"pir": (
TuyaBinarySensorEntityDescription(
key=DPCode.PIR,
device_class=BinarySensorDeviceClass.MOTION,
@@ -230,7 +257,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.PM2_5: (
# PM2.5 Sensor
# https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu
"pm2.5": (
TuyaBinarySensorEntityDescription(
key=DPCode.PM25_STATE,
device_class=BinarySensorDeviceClass.SAFETY,
@@ -238,8 +267,12 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.QXJ: (TAMPER_BINARY_SENSOR,),
DeviceCategory.RQBJ: (
# Temperature and Humidity Sensor with External Probe
# New undocumented category qxj, see https://github.com/home-assistant/core/issues/136472
"qxj": (TAMPER_BINARY_SENSOR,),
# Gas Detector
# https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw
"rqbj": (
TuyaBinarySensorEntityDescription(
key=DPCode.GAS_SENSOR_STATUS,
device_class=BinarySensorDeviceClass.GAS,
@@ -252,14 +285,18 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.SGBJ: (
# Siren Alarm
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"sgbj": (
TuyaBinarySensorEntityDescription(
key=DPCode.CHARGE_STATE,
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.SJ: (
# Water Detector
# https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli
"sj": (
TuyaBinarySensorEntityDescription(
key=DPCode.WATERSENSOR_STATE,
device_class=BinarySensorDeviceClass.MOISTURE,
@@ -267,14 +304,18 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.SOS: (
# Emergency Button
# https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy
"sos": (
TuyaBinarySensorEntityDescription(
key=DPCode.SOS_STATE,
device_class=BinarySensorDeviceClass.SAFETY,
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.VOC: (
# Volatile Organic Compound Sensor
# Note: Undocumented in cloud API docs, based on test device
"voc": (
TuyaBinarySensorEntityDescription(
key=DPCode.VOC_STATE,
device_class=BinarySensorDeviceClass.SAFETY,
@@ -282,7 +323,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.WG2: (
# Gateway control
# https://developer.tuya.com/en/docs/iot/wg?id=Kbcdadk79ejok
"wg2": (
TuyaBinarySensorEntityDescription(
key=DPCode.MASTER_STATE,
device_class=BinarySensorDeviceClass.PROBLEM,
@@ -290,29 +333,39 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
on_value="alarm",
),
),
DeviceCategory.WK: (
# Thermostat
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"wk": (
TuyaBinarySensorEntityDescription(
key=DPCode.VALVE_STATE,
translation_key="valve",
on_value="open",
),
),
DeviceCategory.WKF: (
# Thermostatic Radiator Valve
# Not documented
"wkf": (
TuyaBinarySensorEntityDescription(
key=DPCode.WINDOW_STATE,
device_class=BinarySensorDeviceClass.WINDOW,
on_value="opened",
),
),
DeviceCategory.WSDCG: (TAMPER_BINARY_SENSOR,),
DeviceCategory.YLCG: (
# Temperature and Humidity Sensor
# https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34
"wsdcg": (TAMPER_BINARY_SENSOR,),
# Pressure Sensor
# https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm
"ylcg": (
TuyaBinarySensorEntityDescription(
key=DPCode.PRESSURE_STATE,
on_value="alarm",
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.YWBJ: (
# Smoke Detector
# https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952
"ywbj": (
TuyaBinarySensorEntityDescription(
key=DPCode.SMOKE_SENSOR_STATUS,
device_class=BinarySensorDeviceClass.SMOKE,
@@ -325,7 +378,9 @@ BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ..
),
TAMPER_BINARY_SENSOR,
),
DeviceCategory.ZD: (
# Vibration Sensor
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
"zd": (
TuyaBinarySensorEntityDescription(
key=f"{DPCode.SHOCK_STATE}_vibration",
dpcode=DPCode.SHOCK_STATE,

View File

@@ -103,10 +103,7 @@ class DeviceCategory(StrEnum):
BGL = "bgl"
"""Wall-hung boiler"""
BH = "bh"
"""Smart kettle
https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7
"""
"""Smart kettle"""
BX = "bx"
"""Refrigerator"""
BXX = "bxx"
@@ -133,140 +130,74 @@ class DeviceCategory(StrEnum):
CN = "cn"
"""Milk dispenser"""
CO2BJ = "co2bj"
"""CO2 detector
https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
"""
"""CO2 detector"""
COBJ = "cobj"
"""CO detector
https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v
"""
"""CO detector"""
CS = "cs"
"""Dehumidifier
https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
"""
"""Dehumidifier"""
CWTSWSQ = "cwtswsq"
"""Pet treat feeder"""
CWWQFSQ = "cwwqfsq"
"""Pet ball thrower"""
CWWSQ = "cwwsq"
"""Pet feeder
https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
"""
"""Pet feeder"""
CWYSJ = "cwysj"
"""Pet fountain
https://developer.tuya.com/en/docs/iot/categorycwysj?id=Kaiuz2dfro0nd
"""
"""Pet fountain"""
CZ = "cz"
"""Socket"""
DBL = "dbl"
"""Electric fireplace
https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6
https://developer.tuya.com/en/docs/iot/f?id=Kacpeobojffop
"""
DC = "dc"
"""String lights
# https://developer.tuya.com/en/docs/iot/dc?id=Kaof7taxmvadu
"""
"""String lights"""
DCL = "dcl"
"""Induction cooker"""
DD = "dd"
"""Strip lights
https://developer.tuya.com/en/docs/iot/dd?id=Kaof804aibg2l
"""
"""Strip lights"""
DGNBJ = "dgnbj"
"""Multi-functional alarm
https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
"""
"""Multi-functional alarm"""
DJ = "dj"
"""Light
https://developer.tuya.com/en/docs/iot/categorydj?id=Kaiuyzy3eheyy
"""
"""Light"""
DLQ = "dlq"
"""Circuit breaker
https://developer.tuya.com/en/docs/iot/dlq?id=Kb0kidk9enyh8
"""
"""Circuit breaker"""
DR = "dr"
"""Electric blanket
https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p
"""
"""Electric blanket"""
DS = "ds"
"""TV set"""
FS = "fs"
"""Fan
https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c
"""
"""Fan"""
FSD = "fsd"
"""Ceiling fan light
https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v
"""
"""Ceiling fan light"""
FWD = "fwd"
"""Ambiance light
https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g
"""
"""Ambiance light"""
GGQ = "ggq"
"""Irrigator
https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k
"""
"""Irrigator"""
GYD = "gyd"
"""Motion sensor light
https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy
"""
"""Motion sensor light"""
GYMS = "gyms"
"""Business lock"""
HOTELMS = "hotelms"
"""Hotel lock"""
HPS = "hps"
"""Human presence sensor
https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs
"""
"""Human presence sensor"""
JS = "js"
"""Water purifier"""
JSQ = "jsq"
"""Humidifier
https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b
"""
"""Humidifier"""
JTMSBH = "jtmsbh"
"""Smart lock (keep alive)"""
JTMSPRO = "jtmspro"
"""Residential lock pro"""
JWBJ = "jwbj"
"""Methane detector
https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm
"""
"""Methane detector"""
KFJ = "kfj"
"""Coffee maker
https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f
"""
"""Coffee maker"""
KG = "kg"
"""Switch
https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"""
"""Switch"""
KJ = "kj"
"""Air purifier
https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm
"""
"""Air purifier"""
KQZG = "kqzg"
"""Air fryer"""
KT = "kt"
@@ -277,10 +208,7 @@ class DeviceCategory(StrEnum):
KTKZQ = "ktkzq"
"""Air conditioner controller"""
LDCG = "ldcg"
"""Luminance sensor
https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8
"""
"""Luminance sensor"""
LILIAO = "liliao"
"""Physiotherapy product"""
LYJ = "lyj"
@@ -293,69 +221,42 @@ class DeviceCategory(StrEnum):
MB = "mb"
"""Bread maker"""
MC = "mc"
"""Door/window controller
https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9
"""
"""Door/window controller"""
MCS = "mcs"
"""Contact sensor
https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m
"""
"""Contact sensor"""
MG = "mg"
"""Rice cabinet"""
MJJ = "mjj"
"""Towel rack"""
MK = "mk"
"""Access control
https://developer.tuya.com/en/docs/iot/s?id=Kb0o2xhlkxbet
"""
"""Access control"""
MS = "ms"
"""Residential lock"""
MS_CATEGORY = "ms_category"
"""Lock accessories"""
MSP = "msp"
"""Cat toilet
https://developer.tuya.com/en/docs/iot/s?id=Kakg3srr4ora7
"""
"""Cat toilet"""
MZJ = "mzj"
"""Sous vide cooker
https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux
"""
"""Sous vide cooker"""
NNQ = "nnq"
"""Bottle warmer"""
NTQ = "ntq"
"""HVAC"""
PC = "pc"
"""Power strip
https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"""
"""Power strip"""
PHOTOLOCK = "photolock"
"""Audio and video lock"""
PIR = "pir"
"""Human motion sensor
https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80
"""
"""Human motion sensor"""
PM2_5 = "pm2.5"
"""PM2.5 detector
https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu
"""
"""PM2.5 detector"""
QN = "qn"
"""Heater
https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm
https://developer.tuya.com/en/docs/iot/f?id=K9gf46epy4j82
"""
RQBJ = "rqbj"
"""Gas alarm
https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw
"""
"""Gas alarm"""
RS = "rs"
"""Water heater
@@ -371,40 +272,22 @@ class DeviceCategory(StrEnum):
SF = "sf"
"""Sofa"""
SGBJ = "sgbj"
"""Siren alarm
https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"""
"""Siren alarm"""
SJ = "sj"
"""Water leak detector
https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli
"""
"""Water leak detector"""
SOS = "sos"
"""Emergency button
https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy
"""
"""Emergency button"""
SP = "sp"
"""Smart camera
https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"""
SZ = "sz"
"""Smart indoor garden
https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0
"""
"""Smart indoor garden"""
TGKG = "tgkg"
"""Dimmer switch
https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
"""
"""Dimmer switch"""
TGQ = "tgq"
"""Dimmer
https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
"""
"""Dimmer"""
TNQ = "tnq"
"""Smart milk kettle"""
TRACKER = "tracker"
@@ -412,10 +295,7 @@ class DeviceCategory(StrEnum):
TS = "ts"
"""Smart jump rope"""
TYNDJ = "tyndj"
"""Solar light
https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98
"""
"""Solar light"""
TYY = "tyy"
"""Projector"""
TZC1 = "tzc1"
@@ -428,22 +308,13 @@ class DeviceCategory(StrEnum):
https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"""
WSDCG = "wsdcg"
"""Temperature and humidity sensor
https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34
"""
"""Temperature and humidity sensor"""
XDD = "xdd"
"""Ceiling light
https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r
"""
"""Ceiling light"""
XFJ = "xfj"
"""Ventilation system"""
XXJ = "xxj"
"""Diffuser
https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl
"""
"""Diffuser"""
XY = "xy"
"""Washing machine"""
YB = "yb"
@@ -451,30 +322,15 @@ class DeviceCategory(StrEnum):
YG = "yg"
"""Bathtub"""
YKQ = "ykq"
"""Remote control
https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov
"""
"""Remote control"""
YLCG = "ylcg"
"""Pressure sensor
https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm
"""
"""Pressure sensor"""
YWBJ = "ywbj"
"""Smoke alarm
https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952
"""
"""Smoke alarm"""
ZD = "zd"
"""Vibration sensor
https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
"""
"""Vibration sensor"""
ZNDB = "zndb"
"""Smart electricity meter
https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7
"""
"""Smart electricity meter"""
ZNFH = "znfh"
"""Bento box"""
ZNSB = "znsb"
@@ -483,116 +339,17 @@ class DeviceCategory(StrEnum):
"""Smart pill box"""
# Undocumented
AQCZ = "aqcz"
"""Single Phase power meter (undocumented)"""
BZYD = "bzyd"
"""White noise machine (undocumented)"""
CWJWQ = "cwjwq"
"""Smart Odor Eliminator-Pro (undocumented)
see https://github.com/orgs/home-assistant/discussions/79
"""
DGHSXJ = "dghsxj"
"""Smart Camera - Low power consumption camera (undocumented)
see https://github.com/home-assistant/core/issues/132844
"""
DSD = "dsd"
"""Filament Light
Based on data from https://github.com/home-assistant/core/issues/106703
Product category mentioned in https://developer.tuya.com/en/docs/iot/oemapp-light?id=Kb77kja5woao6
As at 30/12/23 not documented in https://developer.tuya.com/en/docs/iot/lighting?id=Kaiuyzxq30wmc
"""
FSKG = "fskg"
"""Fan wall switch (undocumented)"""
HJJCY = "hjjcy"
"""Air Quality Monitor
https://developer.tuya.com/en/docs/iot/hjjcy?id=Kbeoad8y1nnlv
"""
HXD = "hxd"
"""Wake Up Light II (undocumented)"""
JDCLJQR = "jdcljqr"
"""Curtain Robot (undocumented)"""
JQBJ = "jqbj"
"""Formaldehyde Detector (undocumented)"""
KS = "ks"
"""Tower fan (undocumented)
See https://github.com/orgs/home-assistant/discussions/329
"""
MBD = "mbd"
"""Unknown light product
Found as VECINO RGBW as provided by diagnostics
"""
QCCDZ = "qccdz"
"""AC charging (undocumented)"""
QJDCZ = "qjdcz"
""" Unknown product with light capabilities
Found in some diffusers, plugs and PIR flood lights
"""
QXJ = "qxj"
"""Temperature and Humidity Sensor with External Probe (undocumented)
see https://github.com/home-assistant/core/issues/136472
"""
SFKZQ = "sfkzq"
"""Smart Water Timer (undocumented)"""
SJZ = "sjz"
"""Electric desk (undocumented)"""
SZJCY = "szjcy"
"""Water tester (undocumented)"""
SZJQR = "szjqr"
"""Fingerbot (undocumented)"""
SWTZ = "swtz"
"""Cooking thermometer (undocumented)"""
TDQ = "tdq"
"""Dimmer (undocumented)"""
TYD = "tyd"
"""Outdoor flood light (undocumented)"""
VOC = "voc"
"""Volatile Organic Compound Sensor (undocumented)"""
WG2 = "wg2" # Documented, but not in official list
"""Gateway control
https://developer.tuya.com/en/docs/iot/wg?id=Kbcdadk79ejok
"""
WKCZ = "wkcz"
"""Two-way temperature and humidity switch (undocumented)
"MOES Temperature and Humidity Smart Switch Module MS-103"
"""
WKF = "wkf"
"""Thermostatic Radiator Valve (undocumented)"""
WNYKQ = "wnykq"
"""Smart WiFi IR Remote (undocumented)
eMylo Smart WiFi IR Remote
Air Conditioner Mate (Smart IR Socket)
"""
WXKG = "wxkg" # Documented, but not in official list
"""Wireless Switch
https://developer.tuya.com/en/docs/iot/s?id=Kbeoa9fkv6brp
"""
XNYJCN = "xnyjcn"
"""Micro Storage Inverter
Energy storage and solar PV inverter system with monitoring capabilities
"""
YWCGQ = "ywcgq"
"""Tank Level Sensor (undocumented)"""
ZNNBQ = "znnbq"
"""VESKA-micro inverter (undocumented)"""
ZWJCY = "zwjcy"
"""Soil sensor - plant monitor (undocumented)"""
ZNJXS = "znjxs"
"""Hejhome whitelabel Fingerbot (undocumented)"""
ZNRB = "znrb"
"""Pool HeatPump (undocumented)"""
class DPCode(StrEnum):

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData, IntegerTypeData
from .util import get_dpcode
@@ -40,8 +40,10 @@ class TuyaCoverEntityDescription(CoverEntityDescription):
motor_reverse_mode: DPCode | None = None
COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
DeviceCategory.CKMKZQ: (
COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
# Garage Door Opener
# https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee
"ckmkzq": (
TuyaCoverEntityDescription(
key=DPCode.SWITCH_1,
translation_key="indexed_door",
@@ -67,7 +69,10 @@ COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
device_class=CoverDeviceClass.GARAGE,
),
),
DeviceCategory.CL: (
# Curtain
# Note: Multiple curtains isn't documented
# https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df
"cl": (
TuyaCoverEntityDescription(
key=DPCode.CONTROL,
translation_key="curtain",
@@ -112,7 +117,9 @@ COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
device_class=CoverDeviceClass.BLIND,
),
),
DeviceCategory.CLKG: (
# Curtain Switch
# https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39
"clkg": (
TuyaCoverEntityDescription(
key=DPCode.CONTROL,
translation_key="curtain",
@@ -131,7 +138,9 @@ COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
device_class=CoverDeviceClass.CURTAIN,
),
),
DeviceCategory.JDCLJQR: (
# Curtain Robot
# Note: Not documented
"jdcljqr": (
TuyaCoverEntityDescription(
key=DPCode.CONTROL,
translation_key="curtain",

View File

@@ -14,14 +14,17 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
# All descriptions can be found here. Mostly the Enum data types in the
# default status set of each category (that don't have a set instruction)
# end up being events.
EVENTS: dict[DeviceCategory, tuple[EventEntityDescription, ...]] = {
DeviceCategory.WXKG: (
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
EVENTS: dict[str, tuple[EventEntityDescription, ...]] = {
# Wireless Switch
# https://developer.tuya.com/en/docs/iot/s?id=Kbeoa9fkv6brp
"wxkg": (
EventEntityDescription(
key=DPCode.SWITCH_MODE1,
device_class=EventDeviceClass.BUTTON,

View File

@@ -21,7 +21,7 @@ from homeassistant.util.percentage import (
)
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData, IntegerTypeData
from .util import get_dpcode
@@ -36,13 +36,24 @@ _SPEED_DPCODES = (
)
_SWITCH_DPCODES = (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH)
TUYA_SUPPORT_TYPE: set[DeviceCategory] = {
DeviceCategory.CS,
DeviceCategory.FS,
DeviceCategory.FSD,
DeviceCategory.FSKG,
DeviceCategory.KJ,
DeviceCategory.KS,
TUYA_SUPPORT_TYPE = {
# Dehumidifier
# https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
"cs",
# Fan
# https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c
"fs",
# Ceiling Fan Light
# https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v
"fsd",
# Fan wall switch
"fskg",
# Air Purifier
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm
"kj",
# Undocumented tower fan
# https://github.com/orgs/home-assistant/discussions/329
"ks",
}

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import ActionDPCodeNotFoundError, get_dpcode
@@ -49,15 +49,19 @@ def _has_a_valid_dpcode(
return any(get_dpcode(device, code) for code in properties_to_check)
HUMIDIFIERS: dict[DeviceCategory, TuyaHumidifierEntityDescription] = {
DeviceCategory.CS: TuyaHumidifierEntityDescription(
HUMIDIFIERS: dict[str, TuyaHumidifierEntityDescription] = {
# Dehumidifier
# https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
"cs": TuyaHumidifierEntityDescription(
key=DPCode.SWITCH,
dpcode=(DPCode.SWITCH, DPCode.SWITCH_SPRAY),
current_humidity=DPCode.HUMIDITY_INDOOR,
humidity=DPCode.DEHUMIDITY_SET_VALUE,
device_class=HumidifierDeviceClass.DEHUMIDIFIER,
),
DeviceCategory.JSQ: TuyaHumidifierEntityDescription(
# Humidifier
# https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b
"jsq": TuyaHumidifierEntityDescription(
key=DPCode.SWITCH,
dpcode=(DPCode.SWITCH, DPCode.SWITCH_SPRAY),
current_humidity=DPCode.HUMIDITY_CURRENT,

View File

@@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType, WorkMode
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType, WorkMode
from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import get_dpcode, remap_value
@@ -72,8 +72,9 @@ class TuyaLightEntityDescription(LightEntityDescription):
)
LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
DeviceCategory.BZYD: (
LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = {
# White noise machine
"bzyd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -81,14 +82,18 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.CLKG: (
# Curtain Switch
# https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39
"clkg": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_BACKLIGHT,
translation_key="backlight",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.DC: (
# String Lights
# https://developer.tuya.com/en/docs/iot/dc?id=Kaof7taxmvadu
"dc": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -98,7 +103,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.DD: (
# Strip Lights
# https://developer.tuya.com/en/docs/iot/dd?id=Kaof804aibg2l
"dd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -109,7 +116,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
default_color_type=DEFAULT_COLOR_TYPE_DATA_V2,
),
),
DeviceCategory.DJ: (
# Light
# https://developer.tuya.com/en/docs/iot/categorydj?id=Kaiuyzy3eheyy
"dj": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -127,7 +136,11 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
brightness=DPCode.BRIGHT_VALUE_1,
),
),
DeviceCategory.DSD: (
# Filament Light
# Based on data from https://github.com/home-assistant/core/issues/106703
# Product category mentioned in https://developer.tuya.com/en/docs/iot/oemapp-light?id=Kb77kja5woao6
# As at 30/12/23 not documented in https://developer.tuya.com/en/docs/iot/lighting?id=Kaiuyzxq30wmc
"dsd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -135,7 +148,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
brightness=DPCode.BRIGHT_VALUE,
),
),
DeviceCategory.FS: (
# Fan
# https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c
"fs": (
TuyaLightEntityDescription(
key=DPCode.LIGHT,
name=None,
@@ -150,7 +165,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
brightness=DPCode.BRIGHT_VALUE_1,
),
),
DeviceCategory.FSD: (
# Ceiling Fan Light
# https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v
"fsd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -165,7 +182,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
name=None,
),
),
DeviceCategory.FWD: (
# Ambient Light
# https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g
"fwd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -175,7 +194,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.GYD: (
# Motion Sensor Light
# https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy
"gyd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -185,7 +206,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.HXD: (
# Wake Up Light II
# Not documented
"hxd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
translation_key="light",
@@ -194,7 +217,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
brightness_min=DPCode.BRIGHTNESS_MIN_1,
),
),
DeviceCategory.JSQ: (
# Humidifier Light
# https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b
"jsq": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -203,35 +228,46 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA_HSV,
),
),
DeviceCategory.KG: (
# Switch
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"kg": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_BACKLIGHT,
translation_key="backlight",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.KJ: (
# Air Purifier
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm
"kj": (
TuyaLightEntityDescription(
key=DPCode.LIGHT,
translation_key="backlight",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.KT: (
# Air conditioner
# https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n
"kt": (
TuyaLightEntityDescription(
key=DPCode.LIGHT,
translation_key="backlight",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.KS: (
# Undocumented tower fan
# https://github.com/orgs/home-assistant/discussions/329
"ks": (
TuyaLightEntityDescription(
key=DPCode.LIGHT,
translation_key="backlight",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.MBD: (
# Unknown light product
# Found as VECINO RGBW as provided by diagnostics
# Not documented
"mbd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -240,7 +276,10 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.QJDCZ: (
# Unknown product with light capabilities
# Fond in some diffusers, plugs and PIR flood lights
# Not documented
"qjdcz": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -249,14 +288,18 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.QN: (
# Heater
# https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm
"qn": (
TuyaLightEntityDescription(
key=DPCode.LIGHT,
translation_key="backlight",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SP: (
# Smart Camera
# https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
"sp": (
TuyaLightEntityDescription(
key=DPCode.FLOODLIGHT_SWITCH,
brightness=DPCode.FLOODLIGHT_LIGHTNESS,
@@ -268,14 +311,18 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SZ: (
# Smart Gardening system
# https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0
"sz": (
TuyaLightEntityDescription(
key=DPCode.LIGHT,
brightness=DPCode.BRIGHT_VALUE,
translation_key="light",
),
),
DeviceCategory.TGKG: (
# Dimmer Switch
# https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
"tgkg": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED_1,
translation_key="indexed_light",
@@ -301,7 +348,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
brightness_min=DPCode.BRIGHTNESS_MIN_3,
),
),
DeviceCategory.TGQ: (
# Dimmer
# https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4
"tgq": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
translation_key="light",
@@ -322,7 +371,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
brightness=DPCode.BRIGHT_VALUE_2,
),
),
DeviceCategory.TYD: (
# Outdoor Flood Light
# Not documented
"tyd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -332,7 +383,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.TYNDJ: (
# Solar Light
# https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98
"tyndj": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -342,7 +395,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
color_data=DPCode.COLOUR_DATA,
),
),
DeviceCategory.XDD: (
# Ceiling Light
# https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r
"xdd": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_LED,
name=None,
@@ -356,7 +411,9 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
translation_key="night_light",
),
),
DeviceCategory.YKQ: (
# Remote Control
# https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov
"ykq": (
TuyaLightEntityDescription(
key=DPCode.SWITCH_CONTROLLER,
name=None,
@@ -369,16 +426,19 @@ LIGHTS: dict[DeviceCategory, tuple[TuyaLightEntityDescription, ...]] = {
# Socket (duplicate of `kg`)
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
LIGHTS[DeviceCategory.CZ] = LIGHTS[DeviceCategory.KG]
LIGHTS["cz"] = LIGHTS["kg"]
# Power Socket (duplicate of `kg`)
LIGHTS[DeviceCategory.PC] = LIGHTS[DeviceCategory.KG]
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
LIGHTS["pc"] = LIGHTS["kg"]
# Smart Camera - Low power consumption camera (duplicate of `sp`)
LIGHTS[DeviceCategory.DGHSXJ] = LIGHTS[DeviceCategory.SP]
# Undocumented, see https://github.com/home-assistant/core/issues/132844
LIGHTS["dghsxj"] = LIGHTS["sp"]
# Dimmer (duplicate of `tgq`)
LIGHTS[DeviceCategory.TDQ] = LIGHTS[DeviceCategory.TGQ]
# https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4
LIGHTS["tdq"] = LIGHTS["tgq"]
@dataclass

View File

@@ -21,7 +21,6 @@ from .const import (
DOMAIN,
LOGGER,
TUYA_DISCOVERY_NEW,
DeviceCategory,
DPCode,
DPType,
)
@@ -29,8 +28,13 @@ from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import ActionDPCodeNotFoundError
NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
DeviceCategory.BH: (
# All descriptions can be found here. Mostly the Integer data types in the
# default instructions set of each category end up being a number.
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
# Smart Kettle
# https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7
"bh": (
NumberEntityDescription(
key=DPCode.TEMP_SET,
translation_key="temperature",
@@ -61,14 +65,17 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.BZYD: (
# White noise machine
"bzyd": (
NumberEntityDescription(
key=DPCode.VOLUME_SET,
translation_key="volume",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CO2BJ: (
# CO2 Detector
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
"co2bj": (
NumberEntityDescription(
key=DPCode.ALARM_TIME,
translation_key="alarm_duration",
@@ -77,7 +84,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CWWSQ: (
# Smart Pet Feeder
# https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
"cwwsq": (
NumberEntityDescription(
key=DPCode.MANUAL_FEED,
translation_key="feed",
@@ -87,21 +96,27 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
translation_key="voice_times",
),
),
DeviceCategory.DGNBJ: (
# Multi-functional Sensor
# https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
"dgnbj": (
NumberEntityDescription(
key=DPCode.ALARM_TIME,
translation_key="time",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.FS: (
# Fan
# https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c
"fs": (
NumberEntityDescription(
key=DPCode.TEMP,
translation_key="temperature",
device_class=NumberDeviceClass.TEMPERATURE,
),
),
DeviceCategory.HPS: (
# Human Presence Sensor
# https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs
"hps": (
NumberEntityDescription(
key=DPCode.SENSITIVITY,
translation_key="sensitivity",
@@ -125,7 +140,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
device_class=NumberDeviceClass.DISTANCE,
),
),
DeviceCategory.JSQ: (
# Humidifier
# https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b
"jsq": (
NumberEntityDescription(
key=DPCode.TEMP_SET,
translation_key="temperature",
@@ -137,7 +154,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
device_class=NumberDeviceClass.TEMPERATURE,
),
),
DeviceCategory.KFJ: (
# Coffee maker
# https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f
"kfj": (
NumberEntityDescription(
key=DPCode.WATER_SET,
translation_key="water_level",
@@ -160,7 +179,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.MAL: (
# Alarm Host
# https://developer.tuya.com/en/docs/iot/alarm-hosts?id=K9gf48r87hyjk
"mal": (
NumberEntityDescription(
key=DPCode.DELAY_SET,
# This setting is called "Arm Delay" in the official Tuya app
@@ -182,7 +203,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.MZJ: (
# Sous Vide Cooker
# https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux
"mzj": (
NumberEntityDescription(
key=DPCode.COOK_TEMPERATURE,
translation_key="cook_temperature",
@@ -200,7 +223,8 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SWTZ: (
# Cooking thermometer
"swtz": (
NumberEntityDescription(
key=DPCode.COOK_TEMPERATURE,
translation_key="cook_temperature",
@@ -213,14 +237,17 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SD: (
# Robot Vacuum
# https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo
"sd": (
NumberEntityDescription(
key=DPCode.VOLUME_SET,
translation_key="volume",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SFKZQ: (
# Smart Water Timer
"sfkzq": (
# Controls the irrigation duration for the water valve
NumberEntityDescription(
key=DPCode.COUNTDOWN_1,
@@ -279,21 +306,26 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SGBJ: (
# Siren Alarm
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"sgbj": (
NumberEntityDescription(
key=DPCode.ALARM_TIME,
translation_key="time",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SP: (
# Smart Camera
# https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
"sp": (
NumberEntityDescription(
key=DPCode.BASIC_DEVICE_VOLUME,
translation_key="volume",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SZJQR: (
# Fingerbot
"szjqr": (
NumberEntityDescription(
key=DPCode.ARM_DOWN_PERCENT,
translation_key="move_down",
@@ -312,7 +344,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.TGKG: (
# Dimmer Switch
# https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
"tgkg": (
NumberEntityDescription(
key=DPCode.BRIGHTNESS_MIN_1,
translation_key="indexed_minimum_brightness",
@@ -350,7 +384,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.TGQ: (
# Dimmer Switch
# https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
"tgq": (
NumberEntityDescription(
key=DPCode.BRIGHTNESS_MIN_1,
translation_key="indexed_minimum_brightness",
@@ -376,14 +412,18 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.WK: (
# Thermostat
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"wk": (
NumberEntityDescription(
key=DPCode.TEMP_CORRECTION,
translation_key="temp_correction",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.XNYJCN: (
# Micro Storage Inverter
# Energy storage and solar PV inverter system with monitoring capabilities
"xnyjcn": (
NumberEntityDescription(
key=DPCode.BACKUP_RESERVE,
translation_key="battery_backup_reserve",
@@ -396,7 +436,9 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.YWCGQ: (
# Tank Level Sensor
# Note: Undocumented
"ywcgq": (
NumberEntityDescription(
key=DPCode.MAX_SET,
translation_key="alarm_maximum",
@@ -420,14 +462,17 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.ZD: (
# Vibration Sensor
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
"zd": (
NumberEntityDescription(
key=DPCode.SENSITIVITY,
translation_key="sensitivity",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.ZNRB: (
# Pool HeatPump
"znrb": (
NumberEntityDescription(
key=DPCode.TEMP_SET,
translation_key="temperature",
@@ -437,7 +482,8 @@ NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
}
# Smart Camera - Low power consumption camera (duplicate of `sp`)
NUMBERS[DeviceCategory.DGHSXJ] = NUMBERS[DeviceCategory.SP]
# Undocumented, see https://github.com/home-assistant/core/issues/132844
NUMBERS["dghsxj"] = NUMBERS["sp"]
async def async_setup_entry(

View File

@@ -11,13 +11,16 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
# All descriptions can be found here. Mostly the Enum data types in the
# default instructions set of each category end up being a select.
SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
DeviceCategory.CL: (
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = {
# Curtain
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46o5mtfyc
"cl": (
SelectEntityDescription(
key=DPCode.CONTROL_BACK_MODE,
entity_category=EntityCategory.CONFIG,
@@ -29,14 +32,18 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="curtain_mode",
),
),
DeviceCategory.CO2BJ: (
# CO2 Detector
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
"co2bj": (
SelectEntityDescription(
key=DPCode.ALARM_VOLUME,
translation_key="volume",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CS: (
# Dehumidifier
# https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
"cs": (
SelectEntityDescription(
key=DPCode.COUNTDOWN_SET,
entity_category=EntityCategory.CONFIG,
@@ -48,21 +55,27 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CWJWQ: (
# Smart Odor Eliminator-Pro
# Undocumented, see https://github.com/orgs/home-assistant/discussions/79
"cwjwq": (
SelectEntityDescription(
key=DPCode.WORK_MODE,
entity_category=EntityCategory.CONFIG,
translation_key="odor_elimination_mode",
),
),
DeviceCategory.DGNBJ: (
# Multi-functional Sensor
# https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
"dgnbj": (
SelectEntityDescription(
key=DPCode.ALARM_VOLUME,
translation_key="volume",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.DR: (
# Electric Blanket
# https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p
"dr": (
SelectEntityDescription(
key=DPCode.LEVEL,
icon="mdi:thermometer-lines",
@@ -81,7 +94,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_placeholders={"index": "2"},
),
),
DeviceCategory.FS: (
# Fan
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45vs7vkge
"fs": (
SelectEntityDescription(
key=DPCode.FAN_VERTICAL,
entity_category=EntityCategory.CONFIG,
@@ -103,7 +118,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="countdown",
),
),
DeviceCategory.JSQ: (
# Humidifier
# https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b
"jsq": (
SelectEntityDescription(
key=DPCode.SPRAY_MODE,
entity_category=EntityCategory.CONFIG,
@@ -130,7 +147,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="countdown",
),
),
DeviceCategory.KFJ: (
# Coffee maker
# https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f
"kfj": (
SelectEntityDescription(
key=DPCode.CUP_NUMBER,
translation_key="cups",
@@ -150,7 +169,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="mode",
),
),
DeviceCategory.KG: (
# Switch
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"kg": (
SelectEntityDescription(
key=DPCode.RELAY_STATUS,
entity_category=EntityCategory.CONFIG,
@@ -162,7 +183,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="light_mode",
),
),
DeviceCategory.KJ: (
# Air Purifier
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm
"kj": (
SelectEntityDescription(
key=DPCode.COUNTDOWN,
entity_category=EntityCategory.CONFIG,
@@ -174,13 +197,17 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="countdown",
),
),
DeviceCategory.QN: (
# Heater
# https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm
"qn": (
SelectEntityDescription(
key=DPCode.LEVEL,
translation_key="temperature_level",
),
),
DeviceCategory.SD: (
# Robot Vacuum
# https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo
"sd": (
SelectEntityDescription(
key=DPCode.CISTERN,
entity_category=EntityCategory.CONFIG,
@@ -197,7 +224,8 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="vacuum_mode",
),
),
DeviceCategory.SFKZQ: (
# Smart Water Timer
"sfkzq": (
# Irrigation will not be run within this set delay period
SelectEntityDescription(
key=DPCode.WEATHER_DELAY,
@@ -205,7 +233,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SGBJ: (
# Siren Alarm
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"sgbj": (
SelectEntityDescription(
key=DPCode.ALARM_VOLUME,
translation_key="volume",
@@ -217,7 +247,8 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SJZ: (
# Electric desk
"sjz": (
SelectEntityDescription(
key=DPCode.LEVEL,
translation_key="desk_level",
@@ -229,7 +260,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SP: (
# Smart Camera
# https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
"sp": (
SelectEntityDescription(
key=DPCode.IPC_WORK_MODE,
entity_category=EntityCategory.CONFIG,
@@ -261,14 +294,17 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="motion_sensitivity",
),
),
DeviceCategory.SZJQR: (
# Fingerbot
"szjqr": (
SelectEntityDescription(
key=DPCode.MODE,
entity_category=EntityCategory.CONFIG,
translation_key="fingerbot_mode",
),
),
DeviceCategory.TDQ: (
# IoT Switch?
# Note: Undocumented
"tdq": (
SelectEntityDescription(
key=DPCode.RELAY_STATUS,
entity_category=EntityCategory.CONFIG,
@@ -280,7 +316,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_key="light_mode",
),
),
DeviceCategory.TGKG: (
# Dimmer Switch
# https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
"tgkg": (
SelectEntityDescription(
key=DPCode.RELAY_STATUS,
entity_category=EntityCategory.CONFIG,
@@ -310,7 +348,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_placeholders={"index": "3"},
),
),
DeviceCategory.TGQ: (
# Dimmer
# https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4
"tgq": (
SelectEntityDescription(
key=DPCode.LED_TYPE_1,
entity_category=EntityCategory.CONFIG,
@@ -324,7 +364,9 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
translation_placeholders={"index": "2"},
),
),
DeviceCategory.XNYJCN: (
# Micro Storage Inverter
# Energy storage and solar PV inverter system with monitoring capabilities
"xnyjcn": (
SelectEntityDescription(
key=DPCode.WORK_MODE,
translation_key="inverter_work_mode",
@@ -334,13 +376,16 @@ SELECTS: dict[DeviceCategory, tuple[SelectEntityDescription, ...]] = {
}
# Socket (duplicate of `kg`)
SELECTS[DeviceCategory.CZ] = SELECTS[DeviceCategory.KG]
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
SELECTS["cz"] = SELECTS["kg"]
# Smart Camera - Low power consumption camera (duplicate of `sp`)
SELECTS[DeviceCategory.DGHSXJ] = SELECTS[DeviceCategory.SP]
# Undocumented, see https://github.com/home-assistant/core/issues/132844
SELECTS["dghsxj"] = SELECTS["sp"]
# Power Socket (duplicate of `kg`)
SELECTS[DeviceCategory.PC] = SELECTS[DeviceCategory.KG]
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
SELECTS["pc"] = SELECTS["kg"]
async def async_setup_entry(

View File

@@ -37,7 +37,6 @@ from .const import (
DOMAIN,
LOGGER,
TUYA_DISCOVERY_NEW,
DeviceCategory,
DPCode,
DPType,
UnitOfMeasurement,
@@ -116,8 +115,11 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = (
# All descriptions can be found here. Mostly the Integer data types in the
# default status set of each category (that don't have a set instruction)
# end up being a sensor.
SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
DeviceCategory.AQCZ: (
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
# Single Phase power meter
# Note: Undocumented
"aqcz": (
TuyaSensorEntityDescription(
key=DPCode.CUR_CURRENT,
translation_key="current",
@@ -142,7 +144,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
entity_registry_enabled_default=False,
),
),
DeviceCategory.BH: (
# Smart Kettle
# https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7
"bh": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="current_temperature",
@@ -160,14 +164,18 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="status",
),
),
DeviceCategory.CL: (
# Curtain
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48qy7wkre
"cl": (
TuyaSensorEntityDescription(
key=DPCode.TIME_TOTAL,
translation_key="last_operation_duration",
entity_category=EntityCategory.DIAGNOSTIC,
),
),
DeviceCategory.CO2BJ: (
# CO2 Detector
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
"co2bj": (
TuyaSensorEntityDescription(
key=DPCode.HUMIDITY_VALUE,
translation_key="humidity",
@@ -213,7 +221,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.COBJ: (
# CO Detector
# https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v
"cobj": (
TuyaSensorEntityDescription(
key=DPCode.CO_VALUE,
translation_key="carbon_monoxide",
@@ -223,7 +233,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.CS: (
# Dehumidifier
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r6jke8e
"cs": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_INDOOR,
translation_key="temperature",
@@ -237,21 +249,27 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.CWJWQ: (
# Smart Odor Eliminator-Pro
# Undocumented, see https://github.com/orgs/home-assistant/discussions/79
"cwjwq": (
TuyaSensorEntityDescription(
key=DPCode.WORK_STATE_E,
translation_key="odor_elimination_status",
),
*BATTERY_SENSORS,
),
DeviceCategory.CWWSQ: (
# Smart Pet Feeder
# https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
"cwwsq": (
TuyaSensorEntityDescription(
key=DPCode.FEED_REPORT,
translation_key="last_amount",
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.CWYSJ: (
# Pet Fountain
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r0as4ln
"cwysj": (
TuyaSensorEntityDescription(
key=DPCode.UV_RUNTIME,
translation_key="uv_runtime",
@@ -282,7 +300,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
key=DPCode.WATER_LEVEL, translation_key="water_level_state"
),
),
DeviceCategory.DGNBJ: (
# Multi-functional Sensor
# https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
"dgnbj": (
TuyaSensorEntityDescription(
key=DPCode.GAS_SENSOR_VALUE,
translation_key="gas",
@@ -356,7 +376,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.DLQ: (
# Circuit Breaker
# https://developer.tuya.com/en/docs/iot/dlq?id=Kb0kidk9enyh8
"dlq": (
TuyaSensorEntityDescription(
key=DPCode.TOTAL_FORWARD_ENERGY,
translation_key="total_energy",
@@ -493,7 +515,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
entity_registry_enabled_default=False,
),
),
DeviceCategory.FS: (
# Fan
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48quojr54
"fs": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="temperature",
@@ -501,8 +525,12 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.GGQ: BATTERY_SENSORS,
DeviceCategory.HJJCY: (
# Irrigator
# https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k
"ggq": BATTERY_SENSORS,
# Air Quality Monitor
# https://developer.tuya.com/en/docs/iot/hjjcy?id=Kbeoad8y1nnlv
"hjjcy": (
TuyaSensorEntityDescription(
key=DPCode.AIR_QUALITY_INDEX,
translation_key="air_quality_index",
@@ -553,7 +581,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.JQBJ: (
# Formaldehyde Detector
# Note: Not documented
"jqbj": (
TuyaSensorEntityDescription(
key=DPCode.CO2_VALUE,
translation_key="carbon_dioxide",
@@ -593,7 +623,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.JSQ: (
# Humidifier
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48qwjz0i3
"jsq": (
TuyaSensorEntityDescription(
key=DPCode.HUMIDITY_CURRENT,
translation_key="humidity",
@@ -618,7 +650,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
entity_category=EntityCategory.DIAGNOSTIC,
),
),
DeviceCategory.JWBJ: (
# Methane Detector
# https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm
"jwbj": (
TuyaSensorEntityDescription(
key=DPCode.CH4_SENSOR_VALUE,
translation_key="methane",
@@ -626,7 +660,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.KG: (
# Switch
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"kg": (
TuyaSensorEntityDescription(
key=DPCode.CUR_CURRENT,
translation_key="current",
@@ -663,7 +699,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.TOTAL_INCREASING,
),
),
DeviceCategory.KJ: (
# Air Purifier
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r41mn81
"kj": (
TuyaSensorEntityDescription(
key=DPCode.FILTER,
translation_key="filter_utilization",
@@ -718,7 +756,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="air_quality",
),
),
DeviceCategory.LDCG: (
# Luminance Sensor
# https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8
"ldcg": (
TuyaSensorEntityDescription(
key=DPCode.BRIGHT_STATE,
translation_key="luminosity",
@@ -750,9 +790,15 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.MC: BATTERY_SENSORS,
DeviceCategory.MCS: BATTERY_SENSORS,
DeviceCategory.MSP: (
# Door and Window Controller
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9
"mc": BATTERY_SENSORS,
# Door Window Sensor
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m
"mcs": BATTERY_SENSORS,
# Cat toilet
# https://developer.tuya.com/en/docs/iot/s?id=Kakg3srr4ora7
"msp": (
TuyaSensorEntityDescription(
key=DPCode.CAT_WEIGHT,
translation_key="cat_weight",
@@ -760,7 +806,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.MZJ: (
# Sous Vide Cooker
# https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux
"mzj": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="current_temperature",
@@ -777,8 +825,12 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
native_unit_of_measurement=UnitOfTime.MINUTES,
),
),
DeviceCategory.PIR: BATTERY_SENSORS,
DeviceCategory.PM2_5: (
# PIR Detector
# https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80
"pir": BATTERY_SENSORS,
# PM2.5 Sensor
# https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu
"pm2.5": (
TuyaSensorEntityDescription(
key=DPCode.PM25_VALUE,
translation_key="pm25",
@@ -832,7 +884,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.QN: (
# Heater
# https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm
"qn": (
TuyaSensorEntityDescription(
key=DPCode.WORK_POWER,
translation_key="power",
@@ -840,7 +894,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.QXJ: (
# Temperature and Humidity Sensor with External Probe
# New undocumented category qxj, see https://github.com/home-assistant/core/issues/136472
"qxj": (
TuyaSensorEntityDescription(
key=DPCode.VA_TEMPERATURE,
translation_key="temperature",
@@ -962,7 +1018,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.RQBJ: (
# Gas Detector
# https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw
"rqbj": (
TuyaSensorEntityDescription(
key=DPCode.GAS_SENSOR_VALUE,
name=None,
@@ -971,7 +1029,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.SD: (
# Robot Vacuum
# https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo
"sd": (
TuyaSensorEntityDescription(
key=DPCode.CLEAN_AREA,
translation_key="cleaning_area",
@@ -1025,7 +1085,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.SFKZQ: (
# Smart Water Timer
"sfkzq": (
# Total seconds of irrigation. Read-write value; the device appears to ignore the write action (maybe firmware bug)
TuyaSensorEntityDescription(
key=DPCode.TIME_USE,
@@ -1035,10 +1096,18 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.SGBJ: BATTERY_SENSORS,
DeviceCategory.SJ: BATTERY_SENSORS,
DeviceCategory.SOS: BATTERY_SENSORS,
DeviceCategory.SP: (
# Siren Alarm
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"sgbj": BATTERY_SENSORS,
# Water Detector
# https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli
"sj": BATTERY_SENSORS,
# Emergency Button
# https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy
"sos": BATTERY_SENSORS,
# Smart Camera
# https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
"sp": (
TuyaSensorEntityDescription(
key=DPCode.SENSOR_TEMPERATURE,
translation_key="temperature",
@@ -1059,7 +1128,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.SWTZ: (
# Cooking thermometer
"swtz": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="temperature",
@@ -1075,7 +1145,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.SZ: (
# Smart Gardening system
# https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0
"sz": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="temperature",
@@ -1089,7 +1161,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.SZJCY: (
# Water tester
"szjcy": (
TuyaSensorEntityDescription(
key=DPCode.TDS_IN,
translation_key="total_dissolved_solids",
@@ -1103,8 +1176,11 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.SZJQR: BATTERY_SENSORS,
DeviceCategory.TDQ: (
# Fingerbot
"szjqr": BATTERY_SENSORS,
# IoT Switch
# Note: Undocumented
"tdq": (
TuyaSensorEntityDescription(
key=DPCode.CUR_CURRENT,
translation_key="current",
@@ -1166,8 +1242,12 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.TYNDJ: BATTERY_SENSORS,
DeviceCategory.VOC: (
# Solar Light
# https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98
"tyndj": BATTERY_SENSORS,
# Volatile Organic Compound Sensor
# Note: Undocumented in cloud API docs, based on test device
"voc": (
TuyaSensorEntityDescription(
key=DPCode.CO2_VALUE,
translation_key="carbon_dioxide",
@@ -1207,8 +1287,13 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.WK: (*BATTERY_SENSORS,),
DeviceCategory.WKCZ: (
# Thermostat
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"wk": (*BATTERY_SENSORS,),
# Two-way temperature and humidity switch
# "MOES Temperature and Humidity Smart Switch Module MS-103"
# Documentation not found
"wkcz": (
TuyaSensorEntityDescription(
key=DPCode.HUMIDITY_VALUE,
translation_key="humidity",
@@ -1245,8 +1330,12 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
entity_registry_enabled_default=False,
),
),
DeviceCategory.WKF: BATTERY_SENSORS,
DeviceCategory.WNYKQ: (
# Thermostatic Radiator Valve
# Not documented
"wkf": BATTERY_SENSORS,
# eMylo Smart WiFi IR Remote
# Air Conditioner Mate (Smart IR Socket)
"wnykq": (
TuyaSensorEntityDescription(
key=DPCode.VA_TEMPERATURE,
translation_key="temperature",
@@ -1286,7 +1375,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
entity_registry_enabled_default=False,
),
),
DeviceCategory.WSDCG: (
# Temperature and Humidity Sensor
# https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34
"wsdcg": (
TuyaSensorEntityDescription(
key=DPCode.VA_TEMPERATURE,
translation_key="temperature",
@@ -1319,8 +1410,12 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.WXKG: BATTERY_SENSORS,
DeviceCategory.XNYJCN: (
# Wireless Switch
# https://developer.tuya.com/en/docs/iot/s?id=Kbeoa9fkv6brp
"wxkg": BATTERY_SENSORS, # Pressure Sensor
# Micro Storage Inverter
# Energy storage and solar PV inverter system with monitoring capabilities
"xnyjcn": (
TuyaSensorEntityDescription(
key=DPCode.CURRENT_SOC,
translation_key="battery_soc",
@@ -1391,7 +1486,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.TOTAL_INCREASING,
),
),
DeviceCategory.YLCG: (
# https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm
"ylcg": (
TuyaSensorEntityDescription(
key=DPCode.PRESSURE_VALUE,
name=None,
@@ -1400,7 +1496,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.YWBJ: (
# Smoke Detector
# https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952
"ywbj": (
TuyaSensorEntityDescription(
key=DPCode.SMOKE_SENSOR_VALUE,
translation_key="smoke_amount",
@@ -1409,7 +1507,9 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
),
*BATTERY_SENSORS,
),
DeviceCategory.YWCGQ: (
# Tank Level Sensor
# Note: Undocumented
"ywcgq": (
TuyaSensorEntityDescription(
key=DPCode.LIQUID_STATE,
translation_key="liquid_state",
@@ -1426,8 +1526,12 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.ZD: BATTERY_SENSORS,
DeviceCategory.ZNDB: (
# Vibration Sensor
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
"zd": BATTERY_SENSORS,
# Smart Electricity Meter
# https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7
"zndb": (
TuyaSensorEntityDescription(
key=DPCode.FORWARD_ENERGY_TOTAL,
translation_key="total_energy",
@@ -1543,7 +1647,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
subkey="voltage",
),
),
DeviceCategory.ZNNBQ: (
# VESKA-micro inverter
"znnbq": (
TuyaSensorEntityDescription(
key=DPCode.REVERSE_ENERGY_TOTAL,
translation_key="total_energy",
@@ -1566,7 +1671,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.ZNRB: (
# Pool HeatPump
"znrb": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="temperature",
@@ -1574,7 +1680,8 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
),
),
DeviceCategory.ZWJCY: (
# Soil sensor (Plant monitor)
"zwjcy": (
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="temperature",
@@ -1592,13 +1699,16 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
}
# Socket (duplicate of `kg`)
SENSORS[DeviceCategory.CZ] = SENSORS[DeviceCategory.KG]
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
SENSORS["cz"] = SENSORS["kg"]
# Smart Camera - Low power consumption camera (duplicate of `sp`)
SENSORS[DeviceCategory.DGHSXJ] = SENSORS[DeviceCategory.SP]
# Undocumented, see https://github.com/home-assistant/core/issues/132844
SENSORS["dghsxj"] = SENSORS["sp"]
# Power Socket (duplicate of `kg`)
SENSORS[DeviceCategory.PC] = SENSORS[DeviceCategory.KG]
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
SENSORS["pc"] = SENSORS["kg"]
async def async_setup_entry(

View File

@@ -17,27 +17,37 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .const import TUYA_DISCOVERY_NEW, DPCode
from .entity import TuyaEntity
SIRENS: dict[DeviceCategory, tuple[SirenEntityDescription, ...]] = {
DeviceCategory.CO2BJ: (
# All descriptions can be found here:
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = {
# CO2 Detector
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
"co2bj": (
SirenEntityDescription(
key=DPCode.ALARM_SWITCH,
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.DGNBJ: (
# Multi-functional Sensor
# https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
"dgnbj": (
SirenEntityDescription(
key=DPCode.ALARM_SWITCH,
),
),
DeviceCategory.SGBJ: (
# Siren Alarm
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"sgbj": (
SirenEntityDescription(
key=DPCode.ALARM_SWITCH,
),
),
DeviceCategory.SP: (
# Smart Camera
# https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
"sp": (
SirenEntityDescription(
key=DPCode.SIREN_SWITCH,
),
@@ -45,7 +55,8 @@ SIRENS: dict[DeviceCategory, tuple[SirenEntityDescription, ...]] = {
}
# Smart Camera - Low power consumption camera (duplicate of `sp`)
SIRENS[DeviceCategory.DGHSXJ] = SIRENS[DeviceCategory.SP]
# Undocumented, see https://github.com/home-assistant/core/issues/132844
SIRENS["dghsxj"] = SIRENS["sp"]
async def async_setup_entry(

View File

@@ -25,7 +25,7 @@ from homeassistant.helpers.issue_registry import (
)
from . import TuyaConfigEntry
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .entity import TuyaEntity
@@ -40,8 +40,10 @@ class TuyaDeprecatedSwitchEntityDescription(SwitchEntityDescription):
# All descriptions can be found here. Mostly the Boolean data types in the
# default instruction set of each category end up being a Switch.
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
DeviceCategory.BH: (
SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
# Smart Kettle
# https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7
"bh": (
SwitchEntityDescription(
key=DPCode.START,
translation_key="start",
@@ -52,7 +54,8 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.BZYD: (
# White noise machine
"bzyd": (
SwitchEntityDescription(
key=DPCode.SWITCH,
name=None,
@@ -76,7 +79,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CL: (
# Curtain
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46o5mtfyc
"cl": (
SwitchEntityDescription(
key=DPCode.CONTROL_BACK,
translation_key="reverse",
@@ -88,7 +93,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CN: (
# EasyBaby
# Undocumented, might have a wider use
"cn": (
SwitchEntityDescription(
key=DPCode.DISINFECTION,
translation_key="disinfection",
@@ -98,7 +105,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
translation_key="water",
),
),
DeviceCategory.CS: (
# Dehumidifier
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r6jke8e
"cs": (
SwitchEntityDescription(
key=DPCode.ANION,
translation_key="ionizer",
@@ -118,20 +127,26 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CWJWQ: (
# Smart Odor Eliminator-Pro
# Undocumented, see https://github.com/orgs/home-assistant/discussions/79
"cwjwq": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
),
),
DeviceCategory.CWWSQ: (
# Smart Pet Feeder
# https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
"cwwsq": (
SwitchEntityDescription(
key=DPCode.SLOW_FEED,
translation_key="slow_feed",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.CWYSJ: (
# Pet Fountain
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46aewxem5
"cwysj": (
SwitchEntityDescription(
key=DPCode.FILTER_RESET,
translation_key="filter_reset",
@@ -157,7 +172,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.DJ: (
# Light
# https://developer.tuya.com/en/docs/iot/f?id=K9i5ql3v98hn3
"dj": (
# There are sockets available with an RGB light
# that advertise as `dj`, but provide an additional
# switch to control the plug.
@@ -166,7 +183,8 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
translation_key="plug",
),
),
DeviceCategory.DLQ: (
# Circuit Breaker
"dlq": (
SwitchEntityDescription(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",
@@ -177,7 +195,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
translation_key="switch",
),
),
DeviceCategory.DR: (
# Electric Blanket
# https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p
"dr": (
SwitchEntityDescription(
key=DPCode.SWITCH,
name="Power",
@@ -215,7 +235,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
device_class=SwitchDeviceClass.SWITCH,
),
),
DeviceCategory.FS: (
# Fan
# https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c
"fs": (
SwitchEntityDescription(
key=DPCode.ANION,
translation_key="anion",
@@ -247,14 +269,18 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.FSD: (
# Ceiling Fan Light
# https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v
"fsd": (
SwitchEntityDescription(
key=DPCode.FAN_BEEP,
translation_key="sound",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.GGQ: (
# Irrigator
# https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k
"ggq": (
SwitchEntityDescription(
key=DPCode.SWITCH_1,
translation_key="indexed_switch",
@@ -296,7 +322,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
translation_placeholders={"index": "8"},
),
),
DeviceCategory.HXD: (
# Wake Up Light II
# Not documented
"hxd": (
SwitchEntityDescription(
key=DPCode.SWITCH_1,
translation_key="radio",
@@ -330,7 +358,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
translation_key="sleep_aid",
),
),
DeviceCategory.JSQ: (
# Humidifier
# https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b
"jsq": (
SwitchEntityDescription(
key=DPCode.SWITCH_SOUND,
translation_key="voice",
@@ -347,7 +377,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.KG: (
# Switch
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"kg": (
SwitchEntityDescription(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",
@@ -437,7 +469,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
device_class=SwitchDeviceClass.OUTLET,
),
),
DeviceCategory.KJ: (
# Air Purifier
# https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm
"kj": (
SwitchEntityDescription(
key=DPCode.ANION,
translation_key="ionizer",
@@ -468,7 +502,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.KT: (
# Air conditioner
# https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n
"kt": (
SwitchEntityDescription(
key=DPCode.ANION,
translation_key="ionizer",
@@ -480,13 +516,17 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.KS: (
# Undocumented tower fan
# https://github.com/orgs/home-assistant/discussions/329
"ks": (
SwitchEntityDescription(
key=DPCode.ANION,
translation_key="ionizer",
),
),
DeviceCategory.MAL: (
# Alarm Host
# https://developer.tuya.com/en/docs/iot/alarm-hosts?id=K9gf48r87hyjk
"mal": (
SwitchEntityDescription(
key=DPCode.SWITCH_ALARM_SOUND,
# This switch is called "Arm Beep" in the official Tuya app
@@ -500,7 +540,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.MZJ: (
# Sous Vide Cooker
# https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux
"mzj": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
@@ -512,7 +554,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.PC: (
# Power Socket
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
"pc": (
SwitchEntityDescription(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",
@@ -590,19 +634,26 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
device_class=SwitchDeviceClass.OUTLET,
),
),
DeviceCategory.QCCDZ: (
# AC charging
# Not documented
"qccdz": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
),
),
DeviceCategory.QJDCZ: (
# Unknown product with switch capabilities
# Fond in some diffusers, plugs and PIR flood lights
# Not documented
"qjdcz": (
SwitchEntityDescription(
key=DPCode.SWITCH_1,
translation_key="switch",
),
),
DeviceCategory.QN: (
# Heater
# https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm
"qn": (
SwitchEntityDescription(
key=DPCode.ANION,
translation_key="ionizer",
@@ -614,14 +665,18 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.QXJ: (
# SIREN: Siren (switch) with Temperature and Humidity Sensor with External Probe
# New undocumented category qxj, see https://github.com/home-assistant/core/issues/136472
"qxj": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
device_class=SwitchDeviceClass.OUTLET,
),
),
DeviceCategory.SD: (
# Robot Vacuum
# https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo
"sd": (
SwitchEntityDescription(
key=DPCode.SWITCH_DISTURB,
translation_key="do_not_disturb",
@@ -633,7 +688,8 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SFKZQ: (
# Smart Water Timer
"sfkzq": (
TuyaDeprecatedSwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
@@ -641,21 +697,26 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
breaks_in_ha_version="2026.4.0",
),
),
DeviceCategory.SGBJ: (
# Siren Alarm
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
"sgbj": (
SwitchEntityDescription(
key=DPCode.MUFFLING,
translation_key="mute",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SJZ: (
# Electric desk
"sjz": (
SwitchEntityDescription(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SP: (
# Smart Camera
# https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12
"sp": (
SwitchEntityDescription(
key=DPCode.WIRELESS_BATTERYLOCK,
translation_key="battery_lock",
@@ -712,7 +773,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.SZ: (
# Smart Gardening system
# https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0
"sz": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="power",
@@ -722,13 +785,16 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
translation_key="pump",
),
),
DeviceCategory.SZJQR: (
# Fingerbot
"szjqr": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
),
),
DeviceCategory.TDQ: (
# IoT Switch?
# Note: Undocumented
"tdq": (
SwitchEntityDescription(
key=DPCode.SWITCH_1,
translation_key="indexed_switch",
@@ -771,21 +837,27 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.TYNDJ: (
# Solar Light
# https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98
"tyndj": (
SwitchEntityDescription(
key=DPCode.SWITCH_SAVE_ENERGY,
translation_key="energy_saving",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.WG2: (
# Gateway control
# https://developer.tuya.com/en/docs/iot/wg?id=Kbcdadk79ejok
"wg2": (
SwitchEntityDescription(
key=DPCode.MUFFLING,
translation_key="mute",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.WK: (
# Thermostat
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
"wk": (
SwitchEntityDescription(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",
@@ -797,7 +869,10 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.WKCZ: (
# Two-way temperature and humidity switch
# "MOES Temperature and Humidity Smart Switch Module MS-103"
# Documentation not found
"wkcz": (
SwitchEntityDescription(
key=DPCode.SWITCH_1,
translation_key="indexed_switch",
@@ -811,7 +886,9 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
device_class=SwitchDeviceClass.OUTLET,
),
),
DeviceCategory.WKF: (
# Thermostatic Radiator Valve
# Not documented
"wkf": (
SwitchEntityDescription(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",
@@ -823,34 +900,43 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.WNYKQ: (
# Air Conditioner Mate (Smart IR Socket)
"wnykq": (
SwitchEntityDescription(
key=DPCode.SWITCH,
name=None,
),
),
DeviceCategory.WSDCG: (
# SIREN: Siren (switch) with Temperature and humidity sensor
# https://developer.tuya.com/en/docs/iot/f?id=Kavck4sr3o5ek
"wsdcg": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
device_class=SwitchDeviceClass.OUTLET,
),
),
DeviceCategory.XDD: (
# Ceiling Light
# https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r
"xdd": (
SwitchEntityDescription(
key=DPCode.DO_NOT_DISTURB,
translation_key="do_not_disturb",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.XNYJCN: (
# Micro Storage Inverter
# Energy storage and solar PV inverter system with monitoring capabilities
"xnyjcn": (
SwitchEntityDescription(
key=DPCode.FEEDIN_POWER_LIMIT_ENABLE,
translation_key="output_power_limit",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.XXJ: (
# Diffuser
# https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl
"xxj": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="power",
@@ -865,26 +951,32 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.YWBJ: (
# Smoke Detector
# https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952
"ywbj": (
SwitchEntityDescription(
key=DPCode.MUFFLING,
translation_key="mute",
entity_category=EntityCategory.CONFIG,
),
),
DeviceCategory.ZNDB: (
# Smart Electricity Meter
# https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7
"zndb": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
),
),
DeviceCategory.ZNJXS: (
# Hejhome whitelabel Fingerbot
"znjxs": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
),
),
DeviceCategory.ZNRB: (
# Pool HeatPump
"znrb": (
SwitchEntityDescription(
key=DPCode.SWITCH,
translation_key="switch",
@@ -893,10 +985,12 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
}
# Socket (duplicate of `pc`)
SWITCHES[DeviceCategory.CZ] = SWITCHES[DeviceCategory.PC]
# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s
SWITCHES["cz"] = SWITCHES["pc"]
# Smart Camera - Low power consumption camera (duplicate of `sp`)
SWITCHES[DeviceCategory.DGHSXJ] = SWITCHES[DeviceCategory.SP]
# Undocumented, see https://github.com/home-assistant/core/issues/132844
SWITCHES["dghsxj"] = SWITCHES["sp"]
async def async_setup_entry(

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData
from .util import get_dpcode
@@ -63,7 +63,7 @@ async def async_setup_entry(
entities: list[TuyaVacuumEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if device.category == DeviceCategory.SD:
if device.category == "sd":
entities.append(TuyaVacuumEntity(device, manager))
async_add_entities(entities)

View File

@@ -15,11 +15,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
from .const import TUYA_DISCOVERY_NEW, DPCode
from .entity import TuyaEntity
VALVES: dict[DeviceCategory, tuple[ValveEntityDescription, ...]] = {
DeviceCategory.SFKZQ: (
# All descriptions can be found here. Mostly the Boolean data types in the
# default instruction set of each category end up being a Valve.
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
VALVES: dict[str, tuple[ValveEntityDescription, ...]] = {
# Smart Water Timer
"sfkzq": (
ValveEntityDescription(
key=DPCode.SWITCH,
translation_key="valve",

View File

@@ -364,7 +364,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
if user_input is not None or self._radio_mgr.radio_type in RECOMMENDED_RADIOS:
# ZHA disables the single instance check and will decide at runtime if we
# are migrating or setting up from scratch
if self.hass.config_entries.async_entries(DOMAIN, include_ignore=False):
if self.hass.config_entries.async_entries(DOMAIN):
return await self.async_step_choose_migration_strategy()
return await self.async_step_choose_setup_strategy()
@@ -386,7 +386,7 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
# Allow onboarding for new users to just create a new network automatically
if (
not onboarding.async_is_onboarded(self.hass)
and not self.hass.config_entries.async_entries(DOMAIN, include_ignore=False)
and not self.hass.config_entries.async_entries(DOMAIN)
and not self._radio_mgr.backups
):
return await self.async_step_setup_strategy_recommended()
@@ -438,18 +438,12 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
"""Erase the old radio's network settings before migration."""
# Like in the options flow, pull the correct settings from the config entry
config_entries = self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False
)
config_entries = self.hass.config_entries.async_entries(DOMAIN)
if config_entries:
assert len(config_entries) == 1
config_entry = config_entries[0]
# Unload ZHA before connecting to the old adapter
with suppress(OperationNotAllowed):
await self.hass.config_entries.async_unload(config_entry.entry_id)
# Create a radio manager to connect to the old stick to reset it
temp_radio_mgr = ZhaRadioManager()
temp_radio_mgr.hass = self.hass
@@ -699,9 +693,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
self._set_confirm_only()
zha_config_entries = self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False
)
zha_config_entries = self.hass.config_entries.async_entries(DOMAIN)
# Without confirmation, discovery can automatically progress into parts of the
# config flow logic that interacts with hardware.
@@ -870,9 +862,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
# ZHA is still single instance only, even though we use discovery to allow for
# migrating to a new radio
zha_config_entries = self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False
)
zha_config_entries = self.hass.config_entries.async_entries(DOMAIN)
data = await self._get_config_entry_data()
if len(zha_config_entries) == 1:

View File

@@ -37,7 +37,7 @@ REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = ""
# Format for platform files
PLATFORM_FORMAT: Final = "{platform}.{domain}"
# Explicit reexport to allow other modules to import Platform directly from const
# Type alias to avoid 1000 MyPy errors
Platform = EntityPlatforms
BASE_PLATFORMS: Final = {platform.value for platform in Platform}

View File

@@ -5,15 +5,14 @@ from __future__ import annotations
import abc
import asyncio
from collections import defaultdict
from collections.abc import Callable, Container, Coroutine, Hashable, Iterable, Mapping
from collections.abc import Callable, Container, Hashable, Iterable, Mapping
from contextlib import suppress
import copy
from dataclasses import dataclass
from enum import StrEnum
import functools
import logging
from types import MappingProxyType
from typing import Any, Concatenate, Generic, Required, TypedDict, TypeVar, cast
from typing import Any, Generic, Required, TypedDict, TypeVar, cast
import voluptuous as vol
@@ -151,15 +150,6 @@ class FlowResult(TypedDict, Generic[_FlowContextT, _HandlerT], total=False):
url: str
class ProgressStepData[_FlowResultT](TypedDict):
"""Typed data for progress step tracking."""
tasks: dict[str, asyncio.Task[Any]]
abort_reason: str
abort_description_placeholders: Mapping[str, str]
next_step_result: _FlowResultT | None
def _map_error_to_schema_errors(
schema_errors: dict[str, Any],
error: vol.Invalid,
@@ -649,12 +639,6 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
__progress_task: asyncio.Task[Any] | None = None
__no_progress_task_reported = False
deprecated_show_progress = False
_progress_step_data: ProgressStepData[_FlowResultT] = {
"tasks": {},
"abort_reason": "",
"abort_description_placeholders": MappingProxyType({}),
"next_step_result": None,
}
@property
def source(self) -> str | None:
@@ -777,37 +761,6 @@ class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
description_placeholders=description_placeholders,
)
async def async_step__progress_step_abort(
self, user_input: dict[str, Any] | None = None
) -> _FlowResultT:
"""Abort the flow."""
return self.async_abort(
reason=self._progress_step_data["abort_reason"],
description_placeholders=self._progress_step_data[
"abort_description_placeholders"
],
)
async def async_step__progress_step_progress_done(
self, user_input: dict[str, Any] | None = None
) -> _FlowResultT:
"""Progress done. Return the next step.
Used by the progress_step decorator
to allow decorated step methods
to call the next step method, to change step,
without using async_show_progress_done.
If no next step is set, abort the flow.
"""
if self._progress_step_data["next_step_result"] is None:
return self.async_abort(
reason=self._progress_step_data["abort_reason"],
description_placeholders=self._progress_step_data[
"abort_description_placeholders"
],
)
return self._progress_step_data["next_step_result"]
@callback
def async_external_step(
self,
@@ -977,90 +930,3 @@ class section:
def __call__(self, value: Any) -> Any:
"""Validate input."""
return self.schema(value)
type _FuncType[_T: FlowHandler[Any, Any, Any], _R: FlowResult[Any, Any], **_P] = (
Callable[Concatenate[_T, _P], Coroutine[Any, Any, _R]]
)
def progress_step[
HandlerT: FlowHandler[Any, Any, Any],
ResultT: FlowResult[Any, Any],
**P,
](
description_placeholders: (
dict[str, str] | Callable[[Any], dict[str, str]] | None
) = None,
) -> Callable[[_FuncType[HandlerT, ResultT, P]], _FuncType[HandlerT, ResultT, P]]:
"""Decorator to create a progress step from an async function.
The decorated method should be a step method
which needs to show progress.
The method should accept dict[str, Any] as user_input
and should return a FlowResult or raise AbortFlow.
The method can call self.async_update_progress(progress)
to update progress.
Args:
description_placeholders: Static dict or callable that returns dict for progress UI placeholders.
"""
def decorator(
func: _FuncType[HandlerT, ResultT, P],
) -> _FuncType[HandlerT, ResultT, P]:
@functools.wraps(func)
async def wrapper(
self: FlowHandler[Any, ResultT], *args: P.args, **kwargs: P.kwargs
) -> ResultT:
step_id = func.__name__.replace("async_step_", "")
# Check if we have a progress task running
progress_task = self._progress_step_data["tasks"].get(step_id)
if progress_task is None:
# First call - create and start the progress task
progress_task = self.hass.async_create_task(
func(self, *args, **kwargs), # type: ignore[arg-type]
f"Progress step {step_id}",
)
self._progress_step_data["tasks"][step_id] = progress_task
if not progress_task.done():
# Handle description placeholders
placeholders = None
if description_placeholders is not None:
if callable(description_placeholders):
placeholders = description_placeholders(self)
else:
placeholders = description_placeholders
return self.async_show_progress(
step_id=step_id,
progress_action=step_id,
progress_task=progress_task,
description_placeholders=placeholders,
)
# Task is done or this is a subsequent call
try:
self._progress_step_data["next_step_result"] = await progress_task
except AbortFlow as err:
self._progress_step_data["abort_reason"] = err.reason
self._progress_step_data["abort_description_placeholders"] = (
err.description_placeholders or {}
)
return self.async_show_progress_done(
next_step_id="_progress_step_abort"
)
finally:
# Clean up task reference
self._progress_step_data["tasks"].pop(step_id, None)
return self.async_show_progress_done(
next_step_id="_progress_step_progress_done"
)
return wrapper
return decorator

View File

@@ -127,7 +127,6 @@ FLOWS = {
"coolmaster",
"cpuspeed",
"crownstone",
"cync",
"daikin",
"datadog",
"deako",
@@ -553,7 +552,6 @@ FLOWS = {
"romy",
"roomba",
"roon",
"route_b_smart_meter",
"rova",
"rpi_power",
"ruckus_unleashed",

View File

@@ -1163,12 +1163,6 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"cync": {
"name": "Cync",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push"
},
"dacia": {
"name": "Dacia",
"integration_type": "virtual",
@@ -4174,7 +4168,7 @@
"name": "Manual MQTT Alarm Control Panel"
},
"mqtt": {
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push",
"name": "MQTT"
@@ -4312,11 +4306,6 @@
"integration_type": "virtual",
"supported_by": "home_connect"
},
"neo": {
"name": "Neo",
"integration_type": "virtual",
"supported_by": "shelly"
},
"ness_alarm": {
"name": "Ness Alarm",
"integration_type": "hub",
@@ -5643,12 +5632,6 @@
}
}
},
"route_b_smart_meter": {
"name": "Smart Meter B Route",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"rova": {
"name": "ROVA",
"integration_type": "hub",

View File

@@ -658,19 +658,27 @@ def _get_exposed_entities(
entity_entry = entity_registry.async_get(state.entity_id)
names = [state.name]
device_name = None
area_names = []
if entity_entry is not None:
names.extend(entity_entry.aliases)
device = (
device_registry.async_get(entity_entry.device_id)
if entity_entry.device_id
else None
)
if device:
device_name = device.name_by_user or device.name
if entity_entry.area_id and (
area := area_registry.async_get_area(entity_entry.area_id)
):
# Entity is in area
area_names.append(area.name)
area_names.extend(area.aliases)
elif entity_entry.device_id and (
device := device_registry.async_get(entity_entry.device_id)
):
elif device:
# Check device area
if device.area_id and (
area := area_registry.async_get_area(device.area_id)
@@ -691,6 +699,9 @@ def _get_exposed_entities(
if (parsed_utc := dt_util.parse_datetime(state.state)) is not None:
info["state"] = dt_util.as_local(parsed_utc).isoformat()
if device_name:
info["device"] = device_name
if area_names:
info["areas"] = ", ".join(area_names)

View File

@@ -39,8 +39,8 @@ habluetooth==5.6.4
hass-nabucasa==1.1.1
hassil==3.2.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250924.0
home-assistant-intents==2025.9.24
home-assistant-frontend==20250903.5
home-assistant-intents==2025.9.3
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6

10
mypy.ini generated
View File

@@ -4186,16 +4186,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.route_b_smart_meter.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.rpi_power.*]
check_untyped_defs = true
disallow_incomplete_defs = true

13
requirements_all.txt generated
View File

@@ -1186,10 +1186,10 @@ hole==0.9.0
holidays==0.81
# homeassistant.components.frontend
home-assistant-frontend==20250924.0
home-assistant-frontend==20250903.5
# homeassistant.components.conversation
home-assistant-intents==2025.9.24
home-assistant-intents==2025.9.3
# homeassistant.components.homematicip_cloud
homematicip==2.3.0
@@ -1466,9 +1466,6 @@ moat-ble==0.1.1
# homeassistant.components.moehlenhoff_alpha2
moehlenhoff-alpha2==1.4.0
# homeassistant.components.route_b_smart_meter
momonga==0.1.5
# homeassistant.components.monzo
monzopy==1.5.1
@@ -1929,9 +1926,6 @@ pycsspeechtts==1.0.8
# homeassistant.components.cups
# pycups==2.0.4
# homeassistant.components.cync
pycync==0.4.0
# homeassistant.components.daikin
pydaikin==2.16.0
@@ -2351,7 +2345,6 @@ pyserial-asyncio-fast==0.16
# homeassistant.components.acer_projector
# homeassistant.components.crownstone
# homeassistant.components.route_b_smart_meter
# homeassistant.components.usb
# homeassistant.components.zwave_js
pyserial==3.5
@@ -2692,7 +2685,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.4.1
renault-api==0.4.0
# homeassistant.components.renson
renson-endura-delta==1.7.2

View File

@@ -1035,10 +1035,10 @@ hole==0.9.0
holidays==0.81
# homeassistant.components.frontend
home-assistant-frontend==20250924.0
home-assistant-frontend==20250903.5
# homeassistant.components.conversation
home-assistant-intents==2025.9.24
home-assistant-intents==2025.9.3
# homeassistant.components.homematicip_cloud
homematicip==2.3.0
@@ -1258,9 +1258,6 @@ moat-ble==0.1.1
# homeassistant.components.moehlenhoff_alpha2
moehlenhoff-alpha2==1.4.0
# homeassistant.components.route_b_smart_meter
momonga==0.1.5
# homeassistant.components.monzo
monzopy==1.5.1
@@ -1622,9 +1619,6 @@ pycsspeechtts==1.0.8
# homeassistant.components.cups
# pycups==2.0.4
# homeassistant.components.cync
pycync==0.4.0
# homeassistant.components.daikin
pydaikin==2.16.0
@@ -1963,7 +1957,6 @@ pysensibo==1.2.1
# homeassistant.components.acer_projector
# homeassistant.components.crownstone
# homeassistant.components.route_b_smart_meter
# homeassistant.components.usb
# homeassistant.components.zwave_js
pyserial==3.5
@@ -2241,7 +2234,7 @@ refoss-ha==1.2.5
regenmaschine==2024.03.0
# homeassistant.components.renault
renault-api==0.4.1
renault-api==0.4.0
# homeassistant.components.renson
renson-endura-delta==1.7.2

View File

@@ -32,7 +32,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \
go2rtc-client==0.2.1 \
ha-ffmpeg==3.2.2 \
hassil==3.2.0 \
home-assistant-intents==2025.9.24 \
home-assistant-intents==2025.9.3 \
mutagen==1.47.0 \
pymicro-vad==1.0.1 \
pyspeex-noise==1.0.2

View File

@@ -14,7 +14,7 @@ from homeassistant.components.alexa_devices.const import (
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USERNAME
from .const import TEST_DEVICE, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -48,7 +48,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
CONF_SITE: "https://www.amazon.com",
}
client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: deepcopy(TEST_DEVICE_1)
TEST_SERIAL_NUMBER: deepcopy(TEST_DEVICE)
}
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(
device.device_type

View File

@@ -4,19 +4,20 @@ from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
TEST_CODE = "023123"
TEST_PASSWORD = "fake_password"
TEST_SERIAL_NUMBER = "echo_test_serial_number"
TEST_USERNAME = "fake_email@gmail.com"
TEST_DEVICE_1_SN = "echo_test_serial_number"
TEST_DEVICE_1_ID = "echo_test_device_id"
TEST_DEVICE_1 = AmazonDevice(
TEST_DEVICE_ID = "echo_test_device_id"
TEST_DEVICE = AmazonDevice(
account_name="Echo Test",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=[TEST_DEVICE_1_SN],
device_cluster_members=[TEST_SERIAL_NUMBER],
online=True,
serial_number=TEST_DEVICE_1_SN,
serial_number=TEST_SERIAL_NUMBER,
software_version="echo_test_software_version",
do_not_disturb=False,
response_style=None,
@@ -29,27 +30,3 @@ TEST_DEVICE_1 = AmazonDevice(
)
},
)
TEST_DEVICE_2_SN = "echo_test_2_serial_number"
TEST_DEVICE_2_ID = "echo_test_2_device_id"
TEST_DEVICE_2 = AmazonDevice(
account_name="Echo Test 2",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=[TEST_DEVICE_2_SN],
online=True,
serial_number=TEST_DEVICE_2_SN,
software_version="echo_test_2_software_version",
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A",
sensors={
"temperature": AmazonDeviceSensor(
name="temperature", value="22.5", scale="CELSIUS"
)
},
)

View File

@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -83,7 +83,7 @@ async def test_offline_device(
entity_id = "binary_sensor.echo_test_connectivity"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
@@ -92,7 +92,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)
@@ -101,39 +101,3 @@ async def test_offline_device(
assert (state := hass.states.get(entity_id))
assert state.state != STATE_UNAVAILABLE
async def test_dynamic_device(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test device added dynamically."""
entity_id_1 = "binary_sensor.echo_test_connectivity"
entity_id_2 = "binary_sensor.echo_test_2_connectivity"
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: TEST_DEVICE_1,
}
await setup_integration(hass, mock_config_entry)
assert (state := hass.states.get(entity_id_1))
assert state.state == STATE_ON
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: TEST_DEVICE_1,
TEST_DEVICE_2_SN: TEST_DEVICE_2,
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id_1))
assert state.state == STATE_ON
assert (state := hass.states.get(entity_id_2))
assert state.state == STATE_ON

View File

@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock
from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
@@ -9,7 +10,7 @@ from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from . import setup_integration
from .const import TEST_DEVICE_1, TEST_DEVICE_1_SN, TEST_DEVICE_2, TEST_DEVICE_2_SN
from .const import TEST_DEVICE, TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -26,8 +27,28 @@ async def test_coordinator_stale_device(
entity_id_1 = "binary_sensor.echo_test_2_connectivity"
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: TEST_DEVICE_1,
TEST_DEVICE_2_SN: TEST_DEVICE_2,
TEST_SERIAL_NUMBER: TEST_DEVICE,
"echo_test_2_serial_number_2": AmazonDevice(
account_name="Echo Test 2",
capabilities=["AUDIO_PLAYER", "MICROPHONE"],
device_family="mine",
device_type="echo",
device_owner_customer_id="amazon_ower_id",
device_cluster_members=["echo_test_2_serial_number_2"],
online=True,
serial_number="echo_test_2_serial_number_2",
software_version="echo_test_2_software_version",
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A",
sensors={
"temperature": AmazonDeviceSensor(
name="temperature", value="22.5", scale="CELSIUS"
)
},
),
}
await setup_integration(hass, mock_config_entry)
@@ -38,7 +59,7 @@ async def test_coordinator_stale_device(
assert state.state == STATE_ON
mock_amazon_devices_client.get_devices_data.return_value = {
TEST_DEVICE_1_SN: TEST_DEVICE_1,
TEST_SERIAL_NUMBER: TEST_DEVICE,
}
freezer.tick(SCAN_INTERVAL)

View File

@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_DEVICE_1_SN
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry
from tests.components.diagnostics import (
@@ -54,7 +54,9 @@ async def test_device_diagnostics(
"""Test Amazon device diagnostics."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_DEVICE_1_SN)})
device = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device, repr(device_registry.devices)
assert await get_diagnostics_for_device(

View File

@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_DEVICE_1_SN, TEST_PASSWORD, TEST_USERNAME
from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
from tests.common import MockConfigEntry
@@ -31,7 +31,7 @@ async def test_device_info(
"""Test device registry integration."""
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device_entry is not None
assert device_entry == snapshot

View File

@@ -18,7 +18,7 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import setup_integration
from .const import TEST_DEVICE_1_SN
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -83,7 +83,7 @@ async def test_offline_device(
entity_id = "notify.echo_test_announce"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
@@ -92,7 +92,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)

View File

@@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .const import TEST_DEVICE_1_SN
from .const import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -83,7 +83,7 @@ async def test_offline_device(
entity_id = "sensor.echo_test_temperature"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
@@ -92,7 +92,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)
@@ -133,7 +133,7 @@ async def test_unit_of_measurement(
entity_id = f"sensor.echo_test_{sensor}"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].sensors = {sensor: AmazonDeviceSensor(name=sensor, value=api_value, scale=scale)}
await setup_integration(hass, mock_config_entry)

View File

@@ -19,7 +19,7 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from .const import TEST_DEVICE_1_ID, TEST_DEVICE_1_SN
from .const import TEST_DEVICE_ID, TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, mock_device_registry
@@ -49,7 +49,7 @@ async def test_send_sound_service(
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device_entry
@@ -79,7 +79,7 @@ async def test_send_text_service(
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device_entry
@@ -108,7 +108,7 @@ async def test_send_text_service(
),
(
"wrong_sound_name",
TEST_DEVICE_1_ID,
TEST_DEVICE_ID,
"invalid_sound_value",
{
"sound": "wrong_sound_name",
@@ -128,7 +128,7 @@ async def test_invalid_parameters(
"""Test invalid service parameters."""
device_entry = dr.DeviceEntry(
id=TEST_DEVICE_1_ID, identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
id=TEST_DEVICE_ID, identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
mock_device_registry(
hass,
@@ -164,7 +164,7 @@ async def test_config_entry_not_loaded(
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
identifiers={(DOMAIN, TEST_SERIAL_NUMBER)}
)
assert device_entry

View File

@@ -23,7 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from .conftest import TEST_DEVICE_1_SN
from .conftest import TEST_SERIAL_NUMBER
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -67,7 +67,7 @@ async def test_switch_dnd(
assert mock_amazon_devices_client.set_do_not_disturb.call_count == 1
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].do_not_disturb = True
freezer.tick(SCAN_INTERVAL)
@@ -85,7 +85,7 @@ async def test_switch_dnd(
)
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].do_not_disturb = False
freezer.tick(SCAN_INTERVAL)
@@ -108,7 +108,7 @@ async def test_offline_device(
entity_id = "switch.echo_test_do_not_disturb"
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = False
await setup_integration(hass, mock_config_entry)
@@ -117,7 +117,7 @@ async def test_offline_device(
assert state.state == STATE_UNAVAILABLE
mock_amazon_devices_client.get_devices_data.return_value[
TEST_DEVICE_1_SN
TEST_SERIAL_NUMBER
].online = True
freezer.tick(SCAN_INTERVAL)

View File

@@ -1232,25 +1232,34 @@ async def test_devices_payload_with_entities(
"entities": [
{
"assumed_state": None,
"capabilities": {
"min_color_temp_kelvin": 2000,
"max_color_temp_kelvin": 6535,
},
"domain": "light",
"entity_category": None,
"has_entity_name": True,
"modified_by_integration": None,
"original_device_class": None,
"unit_of_measurement": None,
},
{
"assumed_state": False,
"capabilities": None,
"domain": "number",
"entity_category": "config",
"has_entity_name": True,
"modified_by_integration": None,
"original_device_class": "temperature",
"unit_of_measurement": None,
},
{
"assumed_state": True,
"capabilities": None,
"domain": "light",
"entity_category": None,
"has_entity_name": True,
"modified_by_integration": None,
"original_device_class": None,
"unit_of_measurement": None,
},
@@ -1268,9 +1277,11 @@ async def test_devices_payload_with_entities(
"entities": [
{
"assumed_state": None,
"capabilities": None,
"domain": "light",
"entity_category": None,
"has_entity_name": False,
"modified_by_integration": None,
"original_device_class": None,
"unit_of_measurement": None,
},
@@ -1288,9 +1299,11 @@ async def test_devices_payload_with_entities(
"entities": [
{
"assumed_state": None,
"capabilities": {"state_class": "measurement"},
"domain": "sensor",
"entity_category": None,
"has_entity_name": False,
"modified_by_integration": None,
"original_device_class": "temperature",
"unit_of_measurement": "°C",
},
@@ -1301,9 +1314,11 @@ async def test_devices_payload_with_entities(
"entities": [
{
"assumed_state": None,
"capabilities": None,
"domain": "light",
"entity_category": None,
"has_entity_name": True,
"modified_by_integration": None,
"original_device_class": None,
"unit_of_measurement": None,
},
@@ -1412,9 +1427,11 @@ async def test_analytics_platforms(
"entities": [
{
"assumed_state": None,
"capabilities": {"options": 2},
"domain": "sensor",
"entity_category": None,
"has_entity_name": False,
"modified_by_integration": ["capabilities"],
"original_device_class": None,
"unit_of_measurement": None,
},

View File

@@ -193,53 +193,3 @@ async def test_cover_restore_state(
assert (state := hass.states.get(ENTITY_ID))
assert state.state == STATE_OPENING
async def test_cover_dynamic(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test cover dynamically added."""
mock_serial_bridge.reset_mock()
await setup_integration(hass, mock_serial_bridge_config_entry)
assert hass.states.get(ENTITY_ID)
entity_id_2 = "cover.cover1"
mock_serial_bridge.get_all_devices.return_value[COVER] = {
0: ComelitSerialBridgeObject(
index=0,
name="Cover0",
status=0,
human_status="stopped",
type="cover",
val=0,
protected=0,
zone="Open space",
power=0.0,
power_unit=WATT,
),
1: ComelitSerialBridgeObject(
index=1,
name="Cover1",
status=0,
human_status="stopped",
type="cover",
val=0,
protected=0,
zone="Open space",
power=0.0,
power_unit=WATT,
),
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID)
assert hass.states.get(entity_id_2)

View File

@@ -2,13 +2,9 @@
from unittest.mock import AsyncMock, patch
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.const import LIGHT, WATT
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.comelit.const import SCAN_INTERVAL
from homeassistant.components.light import (
DOMAIN as LIGHT_DOMAIN,
SERVICE_TOGGLE,
@@ -21,7 +17,7 @@ from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.common import MockConfigEntry, snapshot_platform
ENTITY_ID = "light.light0"
@@ -78,53 +74,3 @@ async def test_light_set_state(
assert (state := hass.states.get(ENTITY_ID))
assert state.state == status
async def test_light_dynamic(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test light dynamically added."""
mock_serial_bridge.reset_mock()
await setup_integration(hass, mock_serial_bridge_config_entry)
assert hass.states.get(ENTITY_ID)
entity_id_2 = "light.light1"
mock_serial_bridge.get_all_devices.return_value[LIGHT] = {
0: ComelitSerialBridgeObject(
index=0,
name="Light0",
status=0,
human_status="stopped",
type="light",
val=0,
protected=0,
zone="Open space",
power=0.0,
power_unit=WATT,
),
1: ComelitSerialBridgeObject(
index=1,
name="Light1",
status=0,
human_status="stopped",
type="light",
val=0,
protected=0,
zone="Open space",
power=0.0,
power_unit=WATT,
),
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID)
assert hass.states.get(entity_id_2)

View File

@@ -2,13 +2,8 @@
from unittest.mock import AsyncMock, patch
from aiocomelit.api import (
AlarmDataObject,
ComelitSerialBridgeObject,
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import OTHER, WATT, AlarmAreaState, AlarmZoneState
from aiocomelit.api import AlarmDataObject, ComelitVedoAreaObject, ComelitVedoZoneObject
from aiocomelit.const import AlarmAreaState, AlarmZoneState
from freezegun.api import FrozenDateTimeFactory
from syrupy.assertion import SnapshotAssertion
@@ -49,7 +44,7 @@ async def test_sensor_state_unknown(
mock_vedo: AsyncMock,
mock_vedo_config_entry: MockConfigEntry,
) -> None:
"""Test VEDO sensor unknown state."""
"""Test sensor unknown state."""
await setup_integration(hass, mock_vedo_config_entry)
@@ -93,93 +88,3 @@ async def test_sensor_state_unknown(
assert (state := hass.states.get(ENTITY_ID))
assert state.state == STATE_UNKNOWN
async def test_serial_bridge_sensor_dynamic(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test Serial Bridge sensor dynamically added."""
mock_serial_bridge.reset_mock()
await setup_integration(hass, mock_serial_bridge_config_entry)
entity_id = "sensor.switch0"
entity_id_2 = "sensor.switch1"
assert hass.states.get(entity_id)
mock_serial_bridge.get_all_devices.return_value[OTHER] = {
0: ComelitSerialBridgeObject(
index=0,
name="Switch0",
status=0,
human_status="off",
type="other",
val=0,
protected=0,
zone="Bathroom",
power=0.0,
power_unit=WATT,
),
1: ComelitSerialBridgeObject(
index=1,
name="Switch1",
status=0,
human_status="off",
type="other",
val=0,
protected=0,
zone="Bathroom",
power=0.0,
power_unit=WATT,
),
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(entity_id)
assert hass.states.get(entity_id_2)
async def test_vedo_sensor_dynamic(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_vedo: AsyncMock,
mock_vedo_config_entry: MockConfigEntry,
) -> None:
"""Test VEDO sensor dynamically added."""
mock_vedo.reset_mock()
await setup_integration(hass, mock_vedo_config_entry)
assert hass.states.get(ENTITY_ID)
entity_id_2 = "sensor.zone1"
mock_vedo.get_all_areas_and_zones.return_value["alarm_zones"] = {
0: ComelitVedoZoneObject(
index=0,
name="Zone0",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
),
1: ComelitVedoZoneObject(
index=1,
name="Zone1",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
),
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID)
assert hass.states.get(entity_id_2)

View File

@@ -2,13 +2,9 @@
from unittest.mock import AsyncMock, patch
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.const import IRRIGATION, WATT
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.comelit.const import SCAN_INTERVAL
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TOGGLE,
@@ -21,7 +17,7 @@ from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.common import MockConfigEntry, snapshot_platform
ENTITY_ID = "switch.switch0"
@@ -78,53 +74,3 @@ async def test_switch_set_state(
assert (state := hass.states.get(ENTITY_ID))
assert state.state == status
async def test_switch_dynamic(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test switch dynamically added."""
mock_serial_bridge.reset_mock()
await setup_integration(hass, mock_serial_bridge_config_entry)
entity_id = "switch.switch0"
entity_id_2 = "switch.switch1"
assert hass.states.get(entity_id)
mock_serial_bridge.get_all_devices.return_value[IRRIGATION] = {
0: ComelitSerialBridgeObject(
index=0,
name="Switch0",
status=0,
human_status="off",
type="irrigation",
val=0,
protected=0,
zone="Terrace",
power=0.0,
power_unit=WATT,
),
1: ComelitSerialBridgeObject(
index=1,
name="Switch1",
status=0,
human_status="off",
type="irrigation",
val=0,
protected=0,
zone="Terrace",
power=0.0,
power_unit=WATT,
),
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(entity_id)
assert hass.states.get(entity_id_2)

View File

@@ -2542,7 +2542,7 @@ async def test_non_default_response(hass: HomeAssistant, init_components) -> Non
)
)
assert len(calls) == 1
assert result.response.speech["plain"]["speech"] == "Opening"
assert result.response.speech["plain"]["speech"] == "Opened"
async def test_turn_on_area(

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