Compare commits

...

8 Commits

Author SHA1 Message Date
Matthias Alphart
9c383081f1 Update text.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-20 14:50:09 +01:00
farmio
c5418b4849 Update text.py 2025-12-20 14:37:14 +01:00
farmio
1f53b9183c Support KNX text entity configuration from UI 2025-12-20 14:29:42 +01:00
Paul Tarjan
3050a5c896 Support NVR Hikvision devices (#159253) 2025-12-20 10:08:48 +01:00
Raphael Hehl
9f886e66c7 Update UniFi Protect select entities to use snake_case state values with proper translations (#159284)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-20 10:07:21 +01:00
Tom Harris
3c752d4516 Bump insteon panel to 0.6.0 to fix dialog button issues (#159449) 2025-12-20 10:05:03 +01:00
Raphael Hehl
e4bfdf5b30 Add quality scale configuration for UniFi Protect integration (#157568)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-20 10:03:02 +01:00
Artur Pragacz
3e43424a73 Add myself as codeowner to intent script (#159454) 2025-12-20 10:00:58 +01:00
26 changed files with 465 additions and 82 deletions

2
CODEOWNERS generated
View File

@@ -794,6 +794,8 @@ build.json @home-assistant/supervisor
/tests/components/intellifire/ @jeeftor /tests/components/intellifire/ @jeeftor
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz /homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz /tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/homeassistant/components/intent_script/ @arturpragacz
/tests/components/intent_script/ @arturpragacz
/homeassistant/components/intesishome/ @jnimmo /homeassistant/components/intesishome/ @jnimmo
/homeassistant/components/iometer/ @jukrebs /homeassistant/components/iometer/ @jukrebs
/tests/components/iometer/ @jukrebs /tests/components/iometer/ @jukrebs

View File

@@ -110,7 +110,7 @@ async def async_register_dynalite_frontend(hass: HomeAssistant):
frontend_url_path=DOMAIN, frontend_url_path=DOMAIN,
config_panel_domain=DOMAIN, config_panel_domain=DOMAIN,
webcomponent_name="dynalite-panel", webcomponent_name="dynalite-panel",
module_url=f"{URL_BASE}/entrypoint-{build_id}.js", module_url=f"{URL_BASE}/entrypoint.{build_id}.js",
embed_iframe=True, embed_iframe=True,
require_admin=True, require_admin=True,
) )

View File

@@ -70,6 +70,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
device_type=device_type, device_type=device_type,
) )
# For NVRs or devices with no detected events, try to fetch events from ISAPI
if device_type == "NVR" or not camera.current_event_states:
def fetch_and_inject_nvr_events() -> None:
"""Fetch and inject NVR events in a single executor job."""
if nvr_events := camera.get_event_triggers(None):
camera.inject_events(nvr_events)
await hass.async_add_executor_job(fetch_and_inject_nvr_events)
# Start the event stream # Start the event stream
await hass.async_add_executor_job(camera.start_stream) await hass.async_add_executor_job(camera.start_stream)

View File

@@ -107,7 +107,7 @@ async def async_register_insteon_frontend(hass: HomeAssistant):
frontend_url_path=DOMAIN, frontend_url_path=DOMAIN,
webcomponent_name="insteon-frontend", webcomponent_name="insteon-frontend",
config_panel_domain=DOMAIN, config_panel_domain=DOMAIN,
module_url=f"{URL_BASE}/entrypoint-{build_id}.js", module_url=f"{URL_BASE}/entrypoint.{build_id}.js",
embed_iframe=True, embed_iframe=True,
require_admin=True, require_admin=True,
) )

View File

@@ -282,6 +282,8 @@ async def websocket_reset_properties(
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
return return
for prop in device.configuration.values():
prop.new_value = None
for prop in device.operating_flags: for prop in device.operating_flags:
device.operating_flags[prop].new_value = None device.operating_flags[prop].new_value = None
for prop in device.properties: for prop in device.properties:

View File

@@ -19,7 +19,7 @@
"loggers": ["pyinsteon", "pypubsub"], "loggers": ["pyinsteon", "pypubsub"],
"requirements": [ "requirements": [
"pyinsteon==1.6.4", "pyinsteon==1.6.4",
"insteon-frontend-home-assistant==0.5.0" "insteon-frontend-home-assistant==0.6.0"
], ],
"single_config_entry": true, "single_config_entry": true,
"usb": [ "usb": [

View File

@@ -1,7 +1,7 @@
{ {
"domain": "intent_script", "domain": "intent_script",
"name": "Intent Script", "name": "Intent Script",
"codeowners": [], "codeowners": ["@arturpragacz"],
"documentation": "https://www.home-assistant.io/integrations/intent_script", "documentation": "https://www.home-assistant.io/integrations/intent_script",
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -170,6 +170,7 @@ SUPPORTED_PLATFORMS_UI: Final = {
Platform.LIGHT, Platform.LIGHT,
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
Platform.TEXT,
Platform.TIME, Platform.TIME,
} }

View File

@@ -74,3 +74,6 @@ CONF_GA_SATURATION: Final = "ga_saturation"
# Sensor # Sensor
CONF_ALWAYS_CALLBACK: Final = "always_callback" CONF_ALWAYS_CALLBACK: Final = "always_callback"
# Text
CONF_GA_TEXT: Final = "ga_text"

View File

@@ -13,10 +13,12 @@ from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorStateClass, SensorStateClass,
) )
from homeassistant.components.text import TextMode
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_MODE,
CONF_NAME, CONF_NAME,
CONF_PLATFORM, CONF_PLATFORM,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
@@ -90,6 +92,7 @@ from .const import (
CONF_GA_SWITCH, CONF_GA_SWITCH,
CONF_GA_TEMPERATURE_CURRENT, CONF_GA_TEMPERATURE_CURRENT,
CONF_GA_TEMPERATURE_TARGET, CONF_GA_TEMPERATURE_TARGET,
CONF_GA_TEXT,
CONF_GA_TIME, CONF_GA_TIME,
CONF_GA_UP_DOWN, CONF_GA_UP_DOWN,
CONF_GA_VALVE, CONF_GA_VALVE,
@@ -428,6 +431,20 @@ SWITCH_KNX_SCHEMA = vol.Schema(
}, },
) )
TEXT_KNX_SCHEMA = vol.Schema(
{
vol.Required(CONF_GA_TEXT): GASelector(write_required=True, dpt=["string"]),
vol.Required(CONF_MODE, default=TextMode.TEXT): selector.SelectSelector(
selector.SelectSelectorConfig(
options=list(TextMode),
translation_key="component.knx.config_panel.entities.create.text.knx.mode",
),
),
vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(),
vol.Optional(CONF_SYNC_STATE, default=True): SyncStateSelector(),
},
)
TIME_KNX_SCHEMA = vol.Schema( TIME_KNX_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_GA_TIME): GASelector(write_required=True, valid_dpt="10.001"), vol.Required(CONF_GA_TIME): GASelector(write_required=True, valid_dpt="10.001"),
@@ -696,6 +713,7 @@ KNX_SCHEMA_FOR_PLATFORM = {
Platform.LIGHT: LIGHT_KNX_SCHEMA, Platform.LIGHT: LIGHT_KNX_SCHEMA,
Platform.SENSOR: SENSOR_KNX_SCHEMA, Platform.SENSOR: SENSOR_KNX_SCHEMA,
Platform.SWITCH: SWITCH_KNX_SCHEMA, Platform.SWITCH: SWITCH_KNX_SCHEMA,
Platform.TEXT: TEXT_KNX_SCHEMA,
Platform.TIME: TIME_KNX_SCHEMA, Platform.TIME: TIME_KNX_SCHEMA,
} }

View File

@@ -816,6 +816,23 @@
} }
} }
}, },
"text": {
"description": "The KNX text platform is used as an interface to text objects.",
"knx": {
"ga_text": {
"description": "The group address of the text object.",
"label": "Text"
},
"mode": {
"description": "Select how the entity is displayed in Home Assistant.",
"label": "[%common::config_flow::data::mode%]",
"options": {
"password": "[%common::config_flow::data::password%]",
"text": "[%key:component::text::entity_component::_::state_attributes::mode::state::text%]"
}
}
}
},
"time": { "time": {
"description": "The KNX time platform is used as an interface to time objects.", "description": "The KNX time platform is used as an interface to time objects.",
"knx": { "knx": {

View File

@@ -2,12 +2,12 @@
from __future__ import annotations from __future__ import annotations
from xknx import XKNX from propcache.api import cached_property
from xknx.devices import Notification as XknxNotification from xknx.devices import Notification as XknxNotification
from xknx.dpt import DPTLatin1 from xknx.dpt import DPTLatin1
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.text import TextEntity from homeassistant.components.text import TextEntity, TextMode
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_MODE, CONF_MODE,
@@ -18,13 +18,25 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import CONF_RESPOND_TO_READ, CONF_STATE_ADDRESS, KNX_ADDRESS, KNX_MODULE_KEY from .const import (
from .entity import KnxYamlEntity CONF_RESPOND_TO_READ,
CONF_STATE_ADDRESS,
CONF_SYNC_STATE,
DOMAIN,
KNX_ADDRESS,
KNX_MODULE_KEY,
)
from .entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .knx_module import KNXModule from .knx_module import KNXModule
from .storage.const import CONF_ENTITY, CONF_GA_TEXT
from .storage.util import ConfigExtractor
async def async_setup_entry( async def async_setup_entry(
@@ -32,46 +44,39 @@ async def async_setup_entry(
config_entry: config_entries.ConfigEntry, config_entry: config_entries.ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up sensor(s) for KNX platform.""" """Set up text(s) for KNX platform."""
knx_module = hass.data[KNX_MODULE_KEY] knx_module = hass.data[KNX_MODULE_KEY]
config: list[ConfigType] = knx_module.config_yaml[Platform.TEXT] platform = async_get_current_platform()
knx_module.config_store.add_platform(
async_add_entities(KNXText(knx_module, entity_config) for entity_config in config) platform=Platform.TEXT,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification: entity_platform=platform,
"""Return a KNX Notification to be used within XKNX.""" entity_class=KnxUiText,
return XknxNotification( ),
xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=config[CONF_TYPE],
) )
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := knx_module.config_yaml.get(Platform.TEXT):
entities.extend(
KnxYamlText(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.data["entities"].get(Platform.TEXT):
entities.extend(
KnxUiText(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
)
if entities:
async_add_entities(entities)
class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
class _KnxText(TextEntity, RestoreEntity):
"""Representation of a KNX text.""" """Representation of a KNX text."""
_device: XknxNotification _device: XknxNotification
_attr_native_max = 14 _attr_native_max = 14
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX text."""
super().__init__(
knx_module=knx_module,
device=_create_notification(knx_module.xknx, config),
)
self._attr_mode = config[CONF_MODE]
self._attr_pattern = (
r"[\u0000-\u00ff]*" # Latin-1
if issubclass(self._device.remote_value.dpt_class, DPTLatin1)
else r"[\u0000-\u007f]*" # ASCII
)
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Restore last state.""" """Restore last state."""
await super().async_added_to_hass() await super().async_added_to_hass()
@@ -81,6 +86,15 @@ class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
self._device.remote_value.value = last_state.state self._device.remote_value.value = last_state.state
@cached_property
def pattern(self) -> str | None:
"""Return the regex pattern that the value must match."""
return (
r"[\u0000-\u00ff]*" # Latin-1
if issubclass(self._device.remote_value.dpt_class, DPTLatin1)
else r"[\u0000-\u007f]*" # ASCII
)
@property @property
def native_value(self) -> str | None: def native_value(self) -> str | None:
"""Return the value reported by the text.""" """Return the value reported by the text."""
@@ -89,3 +103,56 @@ class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
async def async_set_value(self, value: str) -> None: async def async_set_value(self, value: str) -> None:
"""Change the value.""" """Change the value."""
await self._device.set(value) await self._device.set(value)
class KnxYamlText(_KnxText, KnxYamlEntity):
"""Representation of a KNX text configured from YAML."""
_device: XknxNotification
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Initialize a KNX text."""
super().__init__(
knx_module=knx_module,
device=XknxNotification(
knx_module.xknx,
name=config[CONF_NAME],
group_address=config[KNX_ADDRESS],
group_address_state=config.get(CONF_STATE_ADDRESS),
respond_to_read=config[CONF_RESPOND_TO_READ],
value_type=config[CONF_TYPE],
),
)
self._attr_mode = config[CONF_MODE]
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
class KnxUiText(_KnxText, KnxUiEntity):
"""Representation of a KNX text configured from UI."""
_device: XknxNotification
def __init__(
self,
knx_module: KNXModule,
unique_id: str,
config: ConfigType,
) -> None:
"""Initialize a KNX text."""
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
knx_conf = ConfigExtractor(config[DOMAIN])
self._device = XknxNotification(
knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address=knx_conf.get_write(CONF_GA_TEXT),
group_address_state=knx_conf.get_state_and_passive(CONF_GA_TEXT),
respond_to_read=knx_conf.get(CONF_RESPOND_TO_READ),
sync_state=knx_conf.get(CONF_SYNC_STATE),
value_type=knx_conf.get_dpt(CONF_GA_TEXT),
)
self._attr_mode = TextMode(knx_conf.get(CONF_MODE))

View File

@@ -40,6 +40,7 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"], "loggers": ["uiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"], "requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"],
"ssdp": [ "ssdp": [
{ {

View File

@@ -0,0 +1,68 @@
rules:
# Bronze
action-setup: done
appropriate-polling:
status: exempt
comment: Integration is push-based using WebSockets (iot_class local_push).
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
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: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage:
status: done
comment: Diagnostics tests could use snapshot testing.
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category: done
entity-device-class:
status: done
comment: "Planned improvement: remove doorbell occupancy binary sensor and keep the event sensor after a solution for https://github.com/home-assistant/core/issues/145941 is available."
entity-disabled-by-default: done
entity-translations:
status: done
comment: "Planned improvement: camera insecure is not translated but will be dropped soon with public api migration"
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: done
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@@ -48,37 +48,37 @@ _KEY_LIGHT_MOTION = "light_motion"
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
HDR_MODES = [ HDR_MODES = [
{"id": "always", "name": "Always On"}, {"id": "always", "name": "always"},
{"id": "off", "name": "Always Off"}, {"id": "off", "name": "off"},
{"id": "auto", "name": "Auto"}, {"id": "auto", "name": "auto"},
] ]
INFRARED_MODES = [ INFRARED_MODES = [
{"id": IRLEDMode.AUTO.value, "name": "Auto"}, {"id": IRLEDMode.AUTO.value, "name": "auto"},
{"id": IRLEDMode.ON.value, "name": "Always Enable"}, {"id": IRLEDMode.ON.value, "name": "on"},
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"}, {"id": IRLEDMode.AUTO_NO_LED.value, "name": "auto_filter_only"},
{"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"}, {"id": IRLEDMode.CUSTOM.value, "name": "custom"},
{"id": IRLEDMode.OFF.value, "name": "Always Disable"}, {"id": IRLEDMode.OFF.value, "name": "off"},
] ]
CHIME_TYPES = [ CHIME_TYPES = [
{"id": ChimeType.NONE.value, "name": "None"}, {"id": ChimeType.NONE.value, "name": "none"},
{"id": ChimeType.MECHANICAL.value, "name": "Mechanical"}, {"id": ChimeType.MECHANICAL.value, "name": "mechanical"},
{"id": ChimeType.DIGITAL.value, "name": "Digital"}, {"id": ChimeType.DIGITAL.value, "name": "digital"},
] ]
MOUNT_TYPES = [ MOUNT_TYPES = [
{"id": MountType.NONE.value, "name": "None"}, {"id": MountType.NONE.value, "name": MountType.NONE.value},
{"id": MountType.DOOR.value, "name": "Door"}, {"id": MountType.DOOR.value, "name": MountType.DOOR.value},
{"id": MountType.WINDOW.value, "name": "Window"}, {"id": MountType.WINDOW.value, "name": MountType.WINDOW.value},
{"id": MountType.GARAGE.value, "name": "Garage"}, {"id": MountType.GARAGE.value, "name": MountType.GARAGE.value},
{"id": MountType.LEAK.value, "name": "Leak"}, {"id": MountType.LEAK.value, "name": MountType.LEAK.value},
] ]
LIGHT_MODE_MOTION = "On Motion - Always" LIGHT_MODE_MOTION = "motion"
LIGHT_MODE_MOTION_DARK = "On Motion - When Dark" LIGHT_MODE_MOTION_DARK = "motion_dark"
LIGHT_MODE_DARK = "When Dark" LIGHT_MODE_DARK = "when_dark"
LIGHT_MODE_OFF = "Manual" LIGHT_MODE_OFF = "manual"
LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF] LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
LIGHT_MODE_TO_SETTINGS = { LIGHT_MODE_TO_SETTINGS = {
@@ -93,13 +93,13 @@ LIGHT_MODE_TO_SETTINGS = {
MOTION_MODE_TO_LIGHT_MODE = [ MOTION_MODE_TO_LIGHT_MODE = [
{"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION}, {"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
{"id": f"{LightModeType.MOTION.value}Dark", "name": LIGHT_MODE_MOTION_DARK}, {"id": f"{LightModeType.MOTION.value}_dark", "name": LIGHT_MODE_MOTION_DARK},
{"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK}, {"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
{"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF}, {"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
] ]
DEVICE_RECORDING_MODES = [ DEVICE_RECORDING_MODES = [
{"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode) {"id": mode.value, "name": mode.value} for mode in list(RecordingMode)
] ]

View File

@@ -350,31 +350,68 @@
}, },
"select": { "select": {
"chime_type": { "chime_type": {
"name": "Chime type" "name": "Chime type",
"state": {
"digital": "Digital",
"mechanical": "Mechanical",
"none": "[%key:common::state::off%]"
}
}, },
"doorbell_text": { "doorbell_text": {
"name": "Doorbell text" "name": "Doorbell text"
}, },
"hdr_mode": { "hdr_mode": {
"name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]" "name": "[%key:component::unifiprotect::entity::binary_sensor::hdr_mode::name%]",
"state": {
"always": "Always on",
"auto": "Auto",
"off": "Always off"
}
}, },
"infrared_mode": { "infrared_mode": {
"name": "Infrared mode" "name": "Infrared mode",
"state": {
"auto": "Auto",
"auto_filter_only": "Auto (filter only, no LEDs)",
"custom": "Auto (custom lux)",
"off": "Always disable",
"on": "Always enable"
}
}, },
"light_mode": { "light_mode": {
"name": "Light mode" "name": "Light mode",
"state": {
"manual": "Manual",
"motion": "On motion - always",
"motion_dark": "On motion - when dark",
"when_dark": "When dark"
}
}, },
"liveview": { "liveview": {
"name": "Liveview" "name": "Liveview"
}, },
"mount_type": { "mount_type": {
"name": "Mount type" "name": "Mount type",
"state": {
"door": "Door",
"garage": "Garage",
"leak": "Leak",
"none": "[%key:common::state::off%]",
"window": "Window"
}
}, },
"paired_camera": { "paired_camera": {
"name": "Paired camera" "name": "Paired camera"
}, },
"recording_mode": { "recording_mode": {
"name": "Recording mode" "name": "Recording mode",
"state": {
"adaptive": "Adaptive",
"always": "Always",
"detections": "Detections",
"never": "Never",
"schedule": "Schedule"
}
} }
}, },
"sensor": { "sensor": {

View File

@@ -104,7 +104,7 @@ def async_get_light_motion_current(obj: Light) -> str:
obj.light_mode_settings.mode is LightModeType.MOTION obj.light_mode_settings.mode is LightModeType.MOTION
and obj.light_mode_settings.enable_at is LightModeEnableType.DARK and obj.light_mode_settings.enable_at is LightModeEnableType.DARK
): ):
return f"{LightModeType.MOTION.value}Dark" return f"{LightModeType.MOTION.value}_dark"
return obj.light_mode_settings.mode.value return obj.light_mode_settings.mode.value

2
requirements_all.txt generated
View File

@@ -1291,7 +1291,7 @@ influxdb==5.3.1
inkbird-ble==1.1.1 inkbird-ble==1.1.1
# homeassistant.components.insteon # homeassistant.components.insteon
insteon-frontend-home-assistant==0.5.0 insteon-frontend-home-assistant==0.6.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==4.2.1 intellifire4py==4.2.1

View File

@@ -1137,7 +1137,7 @@ influxdb==5.3.1
inkbird-ble==1.1.1 inkbird-ble==1.1.1
# homeassistant.components.insteon # homeassistant.components.insteon
insteon-frontend-home-assistant==0.5.0 insteon-frontend-home-assistant==0.6.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==4.2.1 intellifire4py==4.2.1

View File

@@ -1008,7 +1008,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"unifi", "unifi",
"unifi_direct", "unifi_direct",
"unifiled", "unifiled",
"unifiprotect",
"universal", "universal",
"upb", "upb",
"upc_connect", "upc_connect",
@@ -2032,7 +2031,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"unifi", "unifi",
"unifi_direct", "unifi_direct",
"unifiled", "unifiled",
"unifiprotect",
"universal", "universal",
"upb", "upb",
"upc_connect", "upc_connect",

View File

@@ -82,9 +82,20 @@ def mock_hikcamera() -> Generator[MagicMock]:
None, None,
"2024-01-01T00:00:00Z", "2024-01-01T00:00:00Z",
) )
camera.get_event_triggers.return_value = {}
yield hikcamera_mock yield hikcamera_mock
@pytest.fixture
def mock_hik_nvr(mock_hikcamera: MagicMock) -> MagicMock:
"""Return a mocked HikCamera configured as an NVR."""
camera = mock_hikcamera.return_value
camera.get_type = "NVR"
camera.current_event_states = {}
camera.get_event_triggers.return_value = {"Motion": [1, 2]}
return mock_hikcamera
@pytest.fixture @pytest.fixture
async def init_integration( async def init_integration(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_hikcamera: MagicMock hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_hikcamera: MagicMock

View File

@@ -89,3 +89,16 @@ async def test_setup_entry_no_device_id(
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_entry_nvr_fetches_events(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_hik_nvr: MagicMock,
) -> None:
"""Test setup fetches NVR events for NVR devices."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_hik_nvr.return_value.get_event_triggers.assert_called_once()
mock_hik_nvr.return_value.inject_events.assert_called_once()

View File

@@ -0,0 +1,29 @@
{
"version": 2,
"minor_version": 2,
"key": "knx/config_store.json",
"data": {
"entities": {
"text": {
"knx_es_01KCXYT0WEJEQQ13SGCY1NHCYV": {
"entity": {
"name": "test",
"device_info": null,
"entity_category": null
},
"knx": {
"ga_text": {
"write": "1/1/1",
"dpt": "16.001",
"state": "2/2/2",
"passive": []
},
"respond_to_read": false,
"sync_state": true,
"mode": "text"
}
}
}
}
}
}

View File

@@ -1967,6 +1967,69 @@
'type': 'result', 'type': 'result',
}) })
# --- # ---
# name: test_knx_get_schema[text]
dict({
'id': 1,
'result': list([
dict({
'name': 'ga_text',
'options': dict({
'dptClasses': list([
'string',
]),
'passive': True,
'state': dict({
'required': False,
}),
'write': dict({
'required': True,
}),
}),
'required': True,
'type': 'knx_group_address',
}),
dict({
'default': 'text',
'name': 'mode',
'required': True,
'selector': dict({
'select': dict({
'custom_value': False,
'multiple': False,
'options': list([
'password',
'text',
]),
'sort': False,
'translation_key': 'component.knx.config_panel.entities.create.text.knx.mode',
}),
}),
'type': 'ha_selector',
}),
dict({
'default': False,
'name': 'respond_to_read',
'optional': True,
'required': False,
'selector': dict({
'boolean': dict({
}),
}),
'type': 'ha_selector',
}),
dict({
'allow_false': False,
'default': True,
'name': 'sync_state',
'optional': True,
'required': False,
'type': 'knx_sync_state',
}),
]),
'success': True,
'type': 'result',
})
# ---
# name: test_knx_get_schema[time] # name: test_knx_get_schema[time]
dict({ dict({
'id': 1, 'id': 1,

View File

@@ -2,9 +2,11 @@
from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS
from homeassistant.components.knx.schema import TextSchema from homeassistant.components.knx.schema import TextSchema
from homeassistant.const import CONF_NAME from homeassistant.components.text import TextMode
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant, State from homeassistant.core import HomeAssistant, State
from . import KnxEntityGenerator
from .conftest import KNXTestKit from .conftest import KNXTestKit
from tests.common import mock_restore_cache from tests.common import mock_restore_cache
@@ -99,3 +101,44 @@ async def test_text_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit) ->
) )
state = hass.states.get("text.test") state = hass.states.get("text.test")
assert state.state == "hallo" assert state.state == "hallo"
async def test_text_ui_create(
hass: HomeAssistant,
knx: KNXTestKit,
create_ui_entity: KnxEntityGenerator,
) -> None:
"""Test creating a text."""
await knx.setup_integration()
await create_ui_entity(
platform=Platform.TEXT,
entity_data={"name": "test"},
knx_data={
"ga_text": {"write": "1/1/1", "dpt": "16.000"},
"mode": TextMode.PASSWORD,
"sync_state": True,
},
)
await hass.services.async_call(
"text",
"set_value",
{"entity_id": "text.test", "value": "hallo"},
blocking=True,
)
await knx.assert_write(
"1/1/1",
(0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0),
)
knx.assert_state("text.test", "hallo", mode=TextMode.PASSWORD)
async def test_text_ui_load(knx: KNXTestKit) -> None:
"""Test loading a text from storage."""
await knx.setup_integration(config_store_fixture="config_store_text.json")
await knx.assert_read("2/2/2")
await knx.receive_response(
"2/2/2",
(0x68, 0x61, 0x6C, 0x6C, 0x6F, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0),
)
knx.assert_state("text.test", "hallo", mode=TextMode.TEXT)

View File

@@ -95,7 +95,7 @@ async def test_select_setup_light(
await init_entry(hass, ufp, [light]) await init_entry(hass, ufp, [light])
assert_entity_counts(hass, Platform.SELECT, 2, 2) assert_entity_counts(hass, Platform.SELECT, 2, 2)
expected_values = ("On Motion - When Dark", "Not Paired") expected_values = ("motion_dark", "Not Paired")
for index, description in enumerate(LIGHT_SELECTS): for index, description in enumerate(LIGHT_SELECTS):
unique_id, entity_id = await ids_from_device_description( unique_id, entity_id = await ids_from_device_description(
@@ -153,11 +153,11 @@ async def test_select_setup_camera_all(
assert_entity_counts(hass, Platform.SELECT, 5, 5) assert_entity_counts(hass, Platform.SELECT, 5, 5)
expected_values = ( expected_values = (
"Always", "always",
"Auto", "auto",
"Default Message (Welcome)", "Default Message (Welcome)",
"None", "none",
"Always Off", "off",
) )
for index, description in enumerate(CAMERA_SELECTS): for index, description in enumerate(CAMERA_SELECTS):
@@ -186,7 +186,7 @@ async def test_select_setup_camera_none(
await init_entry(hass, ufp, [camera]) await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SELECT, 2, 2) assert_entity_counts(hass, Platform.SELECT, 2, 2)
expected_values = ("Always", "Auto", "Default Message (Welcome)") expected_values = ("always", "auto", "Default Message (Welcome)")
for index, description in enumerate(CAMERA_SELECTS): for index, description in enumerate(CAMERA_SELECTS):
if index == 2: if index == 2:
@@ -403,7 +403,7 @@ async def test_select_set_option_camera_recording(
await hass.services.async_call( await hass.services.async_call(
"select", "select",
"select_option", "select_option",
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Never"}, {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "never"},
blocking=True, blocking=True,
) )
@@ -428,7 +428,7 @@ async def test_select_set_option_camera_ir(
await hass.services.async_call( await hass.services.async_call(
"select", "select",
"select_option", "select_option",
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always Enable"}, {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "on"},
blocking=True, blocking=True,
) )