mirror of
https://github.com/home-assistant/core.git
synced 2026-04-16 22:56:08 +02:00
Compare commits
1 Commits
dev
...
add_durati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb9b42d09 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -362,8 +362,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/deluge/ @tkdrob
|
||||
/homeassistant/components/demo/ @home-assistant/core
|
||||
/tests/components/demo/ @home-assistant/core
|
||||
/homeassistant/components/denon_rs232/ @balloob
|
||||
/tests/components/denon_rs232/ @balloob
|
||||
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney @karwosts
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "denon",
|
||||
"name": "Denon",
|
||||
"integrations": ["denon", "denonavr", "denon_rs232", "heos"]
|
||||
"integrations": ["denon", "denonavr", "heos"]
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ def _make_detected_condition(
|
||||
) -> type[Condition]:
|
||||
"""Create a detected condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
|
||||
STATE_ON,
|
||||
support_duration=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +47,9 @@ def _make_cleared_condition(
|
||||
) -> type[Condition]:
|
||||
"""Create a cleared condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
|
||||
STATE_OFF,
|
||||
support_duration=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -249,6 +249,11 @@
|
||||
.condition_binary_common: &condition_binary_common
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_gas_detected:
|
||||
<<: *condition_binary_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least",
|
||||
@@ -24,6 +25,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
@@ -33,6 +37,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
@@ -54,6 +61,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
@@ -63,6 +73,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
@@ -168,6 +181,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
@@ -177,6 +193,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::air_quality::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
|
||||
@@ -4,6 +4,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
|
||||
Condition,
|
||||
EntityStateConditionBase,
|
||||
make_entity_state_condition,
|
||||
@@ -25,6 +26,7 @@ class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
|
||||
"""State condition."""
|
||||
|
||||
_required_features: int
|
||||
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain with the required features."""
|
||||
@@ -82,9 +84,11 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||
),
|
||||
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
|
||||
"is_disarmed": make_entity_state_condition(
|
||||
DOMAIN, AlarmControlPanelState.DISARMED, support_duration=True
|
||||
),
|
||||
"is_triggered": make_entity_state_condition(
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
target: &condition_common_target
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
fields: &condition_common_fields
|
||||
behavior:
|
||||
behavior: &condition_common_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
@@ -13,10 +13,20 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.condition_common_for: &condition_common_for
|
||||
target: *condition_common_target
|
||||
fields: &condition_common_for_fields
|
||||
behavior: *condition_common_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_armed: *condition_common
|
||||
|
||||
is_armed_away:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -24,7 +34,7 @@ is_armed_away:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
|
||||
is_armed_home:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -32,7 +42,7 @@ is_armed_home:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
||||
|
||||
is_armed_night:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
@@ -40,13 +50,13 @@ is_armed_night:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
|
||||
is_armed_vacation:
|
||||
fields: *condition_common_fields
|
||||
fields: *condition_common_for_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
|
||||
is_disarmed: *condition_common
|
||||
is_disarmed: *condition_common_for
|
||||
|
||||
is_triggered: *condition_common
|
||||
is_triggered: *condition_common_for
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -19,6 +20,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed away"
|
||||
@@ -28,6 +32,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed home"
|
||||
@@ -37,6 +44,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed night"
|
||||
@@ -46,6 +56,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed vacation"
|
||||
@@ -55,6 +68,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is disarmed"
|
||||
@@ -64,6 +80,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is triggered"
|
||||
|
||||
@@ -7,13 +7,17 @@ from .const import DOMAIN
|
||||
from .entity import AssistSatelliteState
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
|
||||
"is_idle": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.IDLE, support_duration=True
|
||||
),
|
||||
"is_listening": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.LISTENING, support_duration=True
|
||||
),
|
||||
"is_processing": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.PROCESSING
|
||||
DOMAIN, AssistSatelliteState.PROCESSING, support_duration=True
|
||||
),
|
||||
"is_responding": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.RESPONDING
|
||||
DOMAIN, AssistSatelliteState.RESPONDING, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_idle: *condition_common
|
||||
is_listening: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is idle"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is listening"
|
||||
@@ -28,6 +35,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is processing"
|
||||
@@ -37,6 +47,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::assist_satellite::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is responding"
|
||||
|
||||
@@ -29,11 +29,17 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_low": make_entity_state_condition(BATTERY_DOMAIN_SPECS, STATE_OFF),
|
||||
"is_charging": make_entity_state_condition(BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON),
|
||||
"is_low": make_entity_state_condition(
|
||||
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_low": make_entity_state_condition(
|
||||
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
"is_charging": make_entity_state_condition(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_charging": make_entity_state_condition(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
"is_level": make_entity_numerical_condition(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for: &condition_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.battery_threshold_entity: &battery_threshold_entity
|
||||
- domain: input_number
|
||||
@@ -39,6 +44,7 @@ is_charging:
|
||||
device_class: battery_charging
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for: *condition_for
|
||||
|
||||
is_not_charging:
|
||||
target:
|
||||
@@ -47,6 +53,7 @@ is_not_charging:
|
||||
device_class: battery_charging
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for: *condition_for
|
||||
|
||||
is_level:
|
||||
target:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least",
|
||||
@@ -12,6 +13,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::battery::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is charging"
|
||||
@@ -33,6 +37,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::battery::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is low"
|
||||
@@ -42,6 +49,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::battery::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is not charging"
|
||||
@@ -51,6 +61,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::battery::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::battery::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Battery is not low"
|
||||
|
||||
@@ -7,7 +7,9 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_event_active": make_entity_state_condition(
|
||||
DOMAIN, STATE_ON, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,3 +12,8 @@ is_event_active:
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least"
|
||||
},
|
||||
"conditions": {
|
||||
"is_event_active": {
|
||||
@@ -8,6 +9,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::calendar::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::calendar::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Calendar event is active"
|
||||
|
||||
@@ -67,7 +67,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_hvac_mode": ClimateHVACModeCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
|
||||
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(
|
||||
DOMAIN,
|
||||
{
|
||||
|
||||
@@ -39,7 +39,16 @@
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
|
||||
is_off: *condition_common
|
||||
is_off:
|
||||
target: *condition_climate_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_on: *condition_common
|
||||
is_cooling: *condition_common
|
||||
is_drying: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least",
|
||||
@@ -52,6 +53,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::climate::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::climate::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device is off"
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"""The Denon RS232 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from denon_rs232 import DenonReceiver, ReceiverState
|
||||
from denon_rs232.models import MODELS
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import LOGGER, DenonRS232ConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DenonRS232ConfigEntry) -> bool:
|
||||
"""Set up Denon RS232 from a config entry."""
|
||||
port = entry.data[CONF_DEVICE]
|
||||
model = MODELS[entry.data[CONF_MODEL]]
|
||||
receiver = DenonReceiver(port, model=model)
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
await receiver.query_state()
|
||||
except (ConnectionError, OSError, TimeoutError) as err:
|
||||
LOGGER.error("Error connecting to Denon receiver at %s: %s", port, err)
|
||||
if receiver.connected:
|
||||
await receiver.disconnect()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = receiver
|
||||
|
||||
@callback
|
||||
def _on_disconnect(state: ReceiverState | None) -> None:
|
||||
# Only reload if the entry is still loaded. During entry removal,
|
||||
# disconnect() fires this callback but the entry is already gone.
|
||||
if state is None and entry.state is ConfigEntryState.LOADED:
|
||||
LOGGER.warning("Denon receiver disconnected, reloading config entry")
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(receiver.subscribe(_on_disconnect))
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DenonRS232ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
await entry.runtime_data.disconnect()
|
||||
|
||||
return unload_ok
|
||||
@@ -1,119 +0,0 @@
|
||||
"""Config flow for the Denon RS232 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from denon_rs232 import DenonReceiver
|
||||
from denon_rs232.models import MODELS
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
SerialSelector,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
CONF_MODEL_NAME = "model_name"
|
||||
|
||||
# Build a flat list of (model_key, individual_name) pairs by splitting
|
||||
# grouped names like "AVR-3803 / AVC-3570 / AVR-2803" into separate entries.
|
||||
# Sorted alphabetically with "Other" at the bottom.
|
||||
MODEL_OPTIONS: list[tuple[str, str]] = sorted(
|
||||
(
|
||||
(_key, _name)
|
||||
for _key, _model in MODELS.items()
|
||||
if _key != "other"
|
||||
for _name in _model.name.split(" / ")
|
||||
),
|
||||
key=lambda x: x[1],
|
||||
)
|
||||
MODEL_OPTIONS.append(("other", "Other"))
|
||||
|
||||
|
||||
async def _async_attempt_connect(port: str, model_key: str) -> str | None:
|
||||
"""Attempt to connect to the receiver at the given port.
|
||||
|
||||
Returns None on success, error on failure.
|
||||
"""
|
||||
model = MODELS[model_key]
|
||||
receiver = DenonReceiver(port, model=model)
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
except (
|
||||
# When the port contains invalid connection data
|
||||
ValueError,
|
||||
# If it is a remote port, and we cannot connect
|
||||
ConnectionError,
|
||||
OSError,
|
||||
TimeoutError,
|
||||
):
|
||||
return "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return "unknown"
|
||||
else:
|
||||
await receiver.disconnect()
|
||||
return None
|
||||
|
||||
|
||||
class DenonRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Denon RS232."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
model_key, _, model_name = user_input[CONF_MODEL].partition(":")
|
||||
resolved_name = model_name if model_key != "other" else None
|
||||
|
||||
self._async_abort_entries_match({CONF_DEVICE: user_input[CONF_DEVICE]})
|
||||
error = await _async_attempt_connect(user_input[CONF_DEVICE], model_key)
|
||||
if not error:
|
||||
return self.async_create_entry(
|
||||
title=resolved_name or "Denon Receiver",
|
||||
data={
|
||||
CONF_DEVICE: user_input[CONF_DEVICE],
|
||||
CONF_MODEL: model_key,
|
||||
CONF_MODEL_NAME: resolved_name,
|
||||
},
|
||||
)
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODEL): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(
|
||||
value=f"{key}:{name}",
|
||||
label=name,
|
||||
)
|
||||
for key, name in MODEL_OPTIONS
|
||||
],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="model",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_DEVICE): SerialSelector(),
|
||||
}
|
||||
),
|
||||
user_input or {},
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
"""Constants for the Denon RS232 integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from denon_rs232 import DenonReceiver
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
DOMAIN = "denon_rs232"
|
||||
|
||||
type DenonRS232ConfigEntry = ConfigEntry[DenonReceiver]
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"domain": "denon_rs232",
|
||||
"name": "Denon RS232",
|
||||
"codeowners": ["@balloob"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/denon_rs232",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denon_rs232"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["denon-rs232==4.1.0"]
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
"""Media player platform for the Denon RS232 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
from denon_rs232 import (
|
||||
MIN_VOLUME_DB,
|
||||
VOLUME_DB_RANGE,
|
||||
DenonReceiver,
|
||||
InputSource,
|
||||
MainPlayer,
|
||||
ReceiverState,
|
||||
ZonePlayer,
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .config_flow import CONF_MODEL_NAME
|
||||
from .const import DOMAIN, DenonRS232ConfigEntry
|
||||
|
||||
INPUT_SOURCE_DENON_TO_HA: dict[InputSource, str] = {
|
||||
InputSource.PHONO: "phono",
|
||||
InputSource.CD: "cd",
|
||||
InputSource.TUNER: "tuner",
|
||||
InputSource.DVD: "dvd",
|
||||
InputSource.VDP: "vdp",
|
||||
InputSource.TV: "tv",
|
||||
InputSource.DBS_SAT: "dbs_sat",
|
||||
InputSource.VCR_1: "vcr_1",
|
||||
InputSource.VCR_2: "vcr_2",
|
||||
InputSource.VCR_3: "vcr_3",
|
||||
InputSource.V_AUX: "v_aux",
|
||||
InputSource.CDR_TAPE1: "cdr_tape1",
|
||||
InputSource.MD_TAPE2: "md_tape2",
|
||||
InputSource.HDP: "hdp",
|
||||
InputSource.DVR: "dvr",
|
||||
InputSource.TV_CBL: "tv_cbl",
|
||||
InputSource.SAT: "sat",
|
||||
InputSource.NET_USB: "net_usb",
|
||||
InputSource.DOCK: "dock",
|
||||
InputSource.IPOD: "ipod",
|
||||
InputSource.BD: "bd",
|
||||
InputSource.SAT_CBL: "sat_cbl",
|
||||
InputSource.MPLAY: "mplay",
|
||||
InputSource.GAME: "game",
|
||||
InputSource.AUX1: "aux1",
|
||||
InputSource.AUX2: "aux2",
|
||||
InputSource.NET: "net",
|
||||
InputSource.BT: "bt",
|
||||
InputSource.USB_IPOD: "usb_ipod",
|
||||
InputSource.EIGHT_K: "eight_k",
|
||||
InputSource.PANDORA: "pandora",
|
||||
InputSource.SIRIUSXM: "siriusxm",
|
||||
InputSource.SPOTIFY: "spotify",
|
||||
InputSource.FLICKR: "flickr",
|
||||
InputSource.IRADIO: "iradio",
|
||||
InputSource.SERVER: "server",
|
||||
InputSource.FAVORITES: "favorites",
|
||||
InputSource.LASTFM: "lastfm",
|
||||
InputSource.XM: "xm",
|
||||
InputSource.SIRIUS: "sirius",
|
||||
InputSource.HDRADIO: "hdradio",
|
||||
InputSource.DAB: "dab",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: DenonRS232ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Denon RS232 media player."""
|
||||
receiver = config_entry.runtime_data
|
||||
entities = [DenonRS232MediaPlayer(receiver, receiver.main, config_entry, "main")]
|
||||
|
||||
if receiver.zone_2.power is not None:
|
||||
entities.append(
|
||||
DenonRS232MediaPlayer(receiver, receiver.zone_2, config_entry, "zone_2")
|
||||
)
|
||||
if receiver.zone_3.power is not None:
|
||||
entities.append(
|
||||
DenonRS232MediaPlayer(receiver, receiver.zone_3, config_entry, "zone_3")
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class DenonRS232MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a Denon receiver controlled over RS232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = MIN_VOLUME_DB
|
||||
_volume_range = VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: DenonReceiver,
|
||||
player: MainPlayer | ZonePlayer,
|
||||
config_entry: DenonRS232ConfigEntry,
|
||||
zone: Literal["main", "zone_2", "zone_3"],
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
self._is_main = zone == "main"
|
||||
|
||||
model = receiver.model
|
||||
assert model is not None # We always set this
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Denon",
|
||||
model_id=config_entry.data.get(CONF_MODEL_NAME),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(
|
||||
INPUT_SOURCE_DENON_TO_HA[source] for source in model.input_sources
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
else:
|
||||
self._attr_name = "Zone 2" if zone == "zone_2" else "Zone 3"
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: ReceiverState | None) -> None:
|
||||
"""Handle a state update from the receiver."""
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
"""Update entity attributes from the shared player object."""
|
||||
if self._player.power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
|
||||
)
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = INPUT_SOURCE_DENON_TO_HA.get(source) if source else None
|
||||
|
||||
volume_min = self._player.volume_min
|
||||
volume_max = self._player.volume_max
|
||||
if volume_min is not None:
|
||||
self._volume_min = volume_min
|
||||
|
||||
if volume_max is not None and volume_max > volume_min:
|
||||
self._volume_range = volume_max - volume_min
|
||||
|
||||
volume = self._player.volume
|
||||
if volume is not None:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
if self._is_main:
|
||||
self._attr_is_volume_muted = cast(MainPlayer, self._player).mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_standby()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
db = volume * self._volume_range + self._volume_min
|
||||
await self._player.set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._player.volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._player.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
player = cast(MainPlayer, self._player)
|
||||
if mute:
|
||||
await player.mute_on()
|
||||
else:
|
||||
await player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
input_source = next(
|
||||
(
|
||||
input_source
|
||||
for input_source, ha_source in INPUT_SOURCE_DENON_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if input_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_input_source(input_source)
|
||||
@@ -1,64 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
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: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
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: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create dynamic devices."
|
||||
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:
|
||||
status: exempt
|
||||
comment: "The integration does not create devices that can become stale."
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -1,84 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::port%]",
|
||||
"model": "Receiver model"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Serial port path to connect to",
|
||||
"model": "Determines available features"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"media_player": {
|
||||
"receiver": {
|
||||
"state_attributes": {
|
||||
"source": {
|
||||
"state": {
|
||||
"aux1": "Aux 1",
|
||||
"aux2": "Aux 2",
|
||||
"bd": "BD Player",
|
||||
"bt": "Bluetooth",
|
||||
"cd": "CD",
|
||||
"cdr_tape1": "CDR/Tape 1",
|
||||
"dab": "DAB",
|
||||
"dbs_sat": "DBS/Sat",
|
||||
"dock": "Dock",
|
||||
"dvd": "DVD",
|
||||
"dvr": "DVR",
|
||||
"eight_k": "8K",
|
||||
"favorites": "Favorites",
|
||||
"flickr": "Flickr",
|
||||
"game": "Game",
|
||||
"hdp": "HDP",
|
||||
"hdradio": "HD Radio",
|
||||
"ipod": "iPod",
|
||||
"iradio": "Internet Radio",
|
||||
"lastfm": "Last.fm",
|
||||
"md_tape2": "MD/Tape 2",
|
||||
"mplay": "Media Player",
|
||||
"net": "HEOS Music",
|
||||
"net_usb": "Network/USB",
|
||||
"pandora": "Pandora",
|
||||
"phono": "Phono",
|
||||
"sat": "Sat",
|
||||
"sat_cbl": "Satellite/Cable",
|
||||
"server": "Server",
|
||||
"sirius": "Sirius",
|
||||
"siriusxm": "SiriusXM",
|
||||
"spotify": "Spotify",
|
||||
"tuner": "Tuner",
|
||||
"tv": "TV Audio",
|
||||
"tv_cbl": "TV/Cable",
|
||||
"usb_ipod": "USB/iPod",
|
||||
"v_aux": "V. Aux",
|
||||
"vcr_1": "VCR 1",
|
||||
"vcr_2": "VCR 2",
|
||||
"vcr_3": "VCR 3",
|
||||
"vdp": "VDP",
|
||||
"xm": "XM"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"model": {
|
||||
"options": {
|
||||
"other": "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_home": make_entity_state_condition(DOMAIN, STATE_HOME),
|
||||
"is_not_home": make_entity_state_condition(DOMAIN, STATE_NOT_HOME),
|
||||
"is_home": make_entity_state_condition(DOMAIN, STATE_HOME, support_duration=True),
|
||||
"is_not_home": make_entity_state_condition(
|
||||
DOMAIN, STATE_NOT_HOME, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_home: *condition_common
|
||||
is_not_home: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::device_tracker::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::device_tracker::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Device tracker is home"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::device_tracker::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::device_tracker::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Device tracker is not home"
|
||||
|
||||
@@ -87,7 +87,6 @@ class MbusDeviceType(IntEnum):
|
||||
GAS = 3
|
||||
HEAT = 4
|
||||
WATER = 7
|
||||
HEAT_COOL = 12
|
||||
|
||||
|
||||
SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
@@ -572,16 +571,6 @@ SENSORS_MBUS_DEVICE_TYPE: dict[int, tuple[DSMRSensorEntityDescription, ...]] = {
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
),
|
||||
MbusDeviceType.HEAT_COOL: (
|
||||
DSMRSensorEntityDescription(
|
||||
key="heat_reading",
|
||||
translation_key="heat_meter_reading",
|
||||
obis_reference="MBUS_METER_READING",
|
||||
is_heat=True,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ class CometBlueCoordinatorData:
|
||||
|
||||
temperatures: dict[str, float | int] = field(default_factory=dict)
|
||||
holiday: dict = field(default_factory=dict)
|
||||
battery: int | None = None
|
||||
|
||||
|
||||
class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorData]):
|
||||
@@ -54,7 +53,6 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
)
|
||||
self.device = cometblue
|
||||
self.address = cometblue.client.address
|
||||
self.data = CometBlueCoordinatorData()
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
@@ -66,11 +64,11 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
LOGGER.debug("Updating device %s with '%s'", self.name, payload)
|
||||
retry_count = 0
|
||||
while retry_count < MAX_RETRIES:
|
||||
retry_count += 1
|
||||
try:
|
||||
async with self.device:
|
||||
return await function(**payload)
|
||||
except (InvalidByteValueError, TimeoutError, BleakError) as ex:
|
||||
retry_count += 1
|
||||
if retry_count >= MAX_RETRIES:
|
||||
raise HomeAssistantError(
|
||||
f"Error sending command to '{self.name}': {ex}"
|
||||
@@ -90,23 +88,20 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
|
||||
async def _async_update_data(self) -> CometBlueCoordinatorData:
|
||||
"""Poll the device."""
|
||||
data = CometBlueCoordinatorData()
|
||||
data: CometBlueCoordinatorData = CometBlueCoordinatorData()
|
||||
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < MAX_RETRIES and not data.temperatures:
|
||||
try:
|
||||
retry_count += 1
|
||||
async with self.device:
|
||||
# temperatures are required and must trigger a retry if not available
|
||||
if not data.temperatures:
|
||||
data.temperatures = await self.device.get_temperature_async()
|
||||
# holiday and battery are optional and should not trigger a retry
|
||||
# holiday is optional and should not trigger a retry
|
||||
try:
|
||||
if not data.holiday:
|
||||
data.holiday = await self.device.get_holiday_async(1) or {}
|
||||
if not data.battery:
|
||||
data.battery = await self.device.get_battery_async()
|
||||
except InvalidByteValueError as ex:
|
||||
LOGGER.warning(
|
||||
"Failed to retrieve optional data for %s: %s (%s)",
|
||||
@@ -115,6 +110,7 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
ex,
|
||||
)
|
||||
except (InvalidByteValueError, TimeoutError, BleakError) as ex:
|
||||
retry_count += 1
|
||||
if retry_count >= MAX_RETRIES:
|
||||
raise UpdateFailed(
|
||||
f"Error retrieving data: {ex}", retry_after=30
|
||||
@@ -132,9 +128,5 @@ class CometBlueDataUpdateCoordinator(DataUpdateCoordinator[CometBlueCoordinatorD
|
||||
) from ex
|
||||
|
||||
# If one value was not retrieved correctly, keep the old value
|
||||
if not data.holiday:
|
||||
data.holiday = self.data.holiday
|
||||
if not data.battery:
|
||||
data.battery = self.data.battery
|
||||
LOGGER.debug("Received data for %s: %s", self.name, data)
|
||||
return data
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"""Comet Blue sensor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import CometBlueConfigEntry, CometBlueDataUpdateCoordinator
|
||||
from .entity import CometBlueBluetoothEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CometBlueConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the client entities."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
entities = [CometBlueBatterySensorEntity(coordinator)]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class CometBlueBatterySensorEntity(CometBlueBluetoothEntity, SensorEntity):
|
||||
"""Representation of a sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CometBlueDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize CometBlueSensorEntity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.address}-{self.entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self.coordinator.data.battery
|
||||
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from . import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::fan::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::fan::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Fan is off"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::fan::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::fan::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Fan is on"
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
"disk_free": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"disk_size": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
"disk_usage": {
|
||||
"default": "mdi:harddisk"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["glances_api"],
|
||||
"requirements": ["glances-api==0.10.0"]
|
||||
"requirements": ["glances-api==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -49,14 +49,6 @@ SENSOR_TYPES = {
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("fs", "disk_size"): GlancesSensorEntityDescription(
|
||||
key="disk_size",
|
||||
type="fs",
|
||||
translation_key="disk_size",
|
||||
native_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
("fs", "disk_free"): GlancesSensorEntityDescription(
|
||||
key="disk_free",
|
||||
type="fs",
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
"disk_free": {
|
||||
"name": "{sensor_label} disk free"
|
||||
},
|
||||
"disk_size": {
|
||||
"name": "{sensor_label} disk size"
|
||||
},
|
||||
"disk_usage": {
|
||||
"name": "{sensor_label} disk usage"
|
||||
},
|
||||
|
||||
@@ -70,8 +70,8 @@ class IsModeCondition(EntityStateConditionBase):
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
|
||||
"is_drying": make_entity_state_condition(
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.DRYING
|
||||
),
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
.condition_common_for: &condition_common_for
|
||||
target: *condition_humidifier_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.humidity_threshold_entity: &humidity_threshold_entity
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
@@ -27,8 +37,8 @@
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
is_off: *condition_common_for
|
||||
is_on: *condition_common_for
|
||||
is_drying: *condition_common
|
||||
is_humidifying: *condition_common
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
@@ -42,6 +43,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::humidifier::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Humidifier is off"
|
||||
@@ -51,6 +55,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::humidifier::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Humidifier is on"
|
||||
|
||||
@@ -25,10 +25,10 @@ ILLUMINANCE_VALUE_DOMAIN_SPECS = {
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_detected": make_entity_state_condition(
|
||||
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_ON
|
||||
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_detected": make_entity_state_condition(
|
||||
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_OFF
|
||||
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
"is_value": make_entity_numerical_condition(
|
||||
ILLUMINANCE_VALUE_DOMAIN_SPECS, LIGHT_LUX
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_detected: *detected_condition_common
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least",
|
||||
@@ -12,6 +13,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::illuminance::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::illuminance::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Light is detected"
|
||||
@@ -21,6 +25,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::illuminance::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::illuminance::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Light is not detected"
|
||||
|
||||
@@ -6,13 +6,21 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN, LawnMowerActivity
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_docked": make_entity_state_condition(DOMAIN, LawnMowerActivity.DOCKED),
|
||||
"is_encountering_an_error": make_entity_state_condition(
|
||||
DOMAIN, LawnMowerActivity.ERROR
|
||||
"is_docked": make_entity_state_condition(
|
||||
DOMAIN, LawnMowerActivity.DOCKED, support_duration=True
|
||||
),
|
||||
"is_encountering_an_error": make_entity_state_condition(
|
||||
DOMAIN, LawnMowerActivity.ERROR, support_duration=True
|
||||
),
|
||||
"is_mowing": make_entity_state_condition(
|
||||
DOMAIN, LawnMowerActivity.MOWING, support_duration=True
|
||||
),
|
||||
"is_paused": make_entity_state_condition(
|
||||
DOMAIN, LawnMowerActivity.PAUSED, support_duration=True
|
||||
),
|
||||
"is_returning": make_entity_state_condition(
|
||||
DOMAIN, LawnMowerActivity.RETURNING, support_duration=True
|
||||
),
|
||||
"is_mowing": make_entity_state_condition(DOMAIN, LawnMowerActivity.MOWING),
|
||||
"is_paused": make_entity_state_condition(DOMAIN, LawnMowerActivity.PAUSED),
|
||||
"is_returning": make_entity_state_condition(DOMAIN, LawnMowerActivity.RETURNING),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_docked: *condition_common
|
||||
is_encountering_an_error: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lawn mower is docked"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lawn mower is encountering an error"
|
||||
@@ -28,6 +35,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lawn mower is mowing"
|
||||
@@ -37,6 +47,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lawn mower is paused"
|
||||
@@ -46,6 +59,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lawn_mower::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lawn mower is returning"
|
||||
|
||||
@@ -38,8 +38,8 @@ class BrightnessCondition(EntityNumericalConditionBase):
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_brightness": BrightnessCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.brightness_threshold_entity: &brightness_threshold_entity
|
||||
- domain: input_number
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"field_brightness_description": "Number indicating brightness, where 0 turns the light off, 1 is the minimum brightness, and 255 is the maximum brightness.",
|
||||
"field_brightness_name": "Brightness value",
|
||||
@@ -57,6 +58,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::light::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::light::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Light is off"
|
||||
@@ -66,6 +70,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::light::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::light::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Light is on"
|
||||
|
||||
@@ -6,10 +6,18 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN, LockState
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_jammed": make_entity_state_condition(DOMAIN, LockState.JAMMED),
|
||||
"is_locked": make_entity_state_condition(DOMAIN, LockState.LOCKED),
|
||||
"is_open": make_entity_state_condition(DOMAIN, LockState.OPEN),
|
||||
"is_unlocked": make_entity_state_condition(DOMAIN, LockState.UNLOCKED),
|
||||
"is_jammed": make_entity_state_condition(
|
||||
DOMAIN, LockState.JAMMED, support_duration=True
|
||||
),
|
||||
"is_locked": make_entity_state_condition(
|
||||
DOMAIN, LockState.LOCKED, support_duration=True
|
||||
),
|
||||
"is_open": make_entity_state_condition(
|
||||
DOMAIN, LockState.OPEN, support_duration=True
|
||||
),
|
||||
"is_unlocked": make_entity_state_condition(
|
||||
DOMAIN, LockState.UNLOCKED, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_jammed: *condition_common
|
||||
is_locked: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lock::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lock::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock is jammed"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lock::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lock::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock is locked"
|
||||
@@ -28,6 +35,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lock::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lock::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock is open"
|
||||
@@ -37,6 +47,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::lock::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::lock::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Lock is unlocked"
|
||||
|
||||
@@ -6,7 +6,9 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN, MediaPlayerState
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, MediaPlayerState.OFF),
|
||||
"is_off": make_entity_state_condition(
|
||||
DOMAIN, MediaPlayerState.OFF, support_duration=True
|
||||
),
|
||||
"is_on": make_entity_state_condition(
|
||||
DOMAIN,
|
||||
{
|
||||
@@ -27,8 +29,12 @@ CONDITIONS: dict[str, type[Condition]] = {
|
||||
MediaPlayerState.PAUSED,
|
||||
},
|
||||
),
|
||||
"is_paused": make_entity_state_condition(DOMAIN, MediaPlayerState.PAUSED),
|
||||
"is_playing": make_entity_state_condition(DOMAIN, MediaPlayerState.PLAYING),
|
||||
"is_paused": make_entity_state_condition(
|
||||
DOMAIN, MediaPlayerState.PAUSED, support_duration=True
|
||||
),
|
||||
"is_playing": make_entity_state_condition(
|
||||
DOMAIN, MediaPlayerState.PLAYING, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
target: &condition_media_player_target
|
||||
entity:
|
||||
domain: media_player
|
||||
fields:
|
||||
behavior:
|
||||
behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
@@ -13,8 +13,18 @@
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
.condition_common_with_for: &condition_common_with_for
|
||||
target: *condition_media_player_target
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_off: *condition_common_with_for
|
||||
is_on: *condition_common
|
||||
is_not_playing: *condition_common
|
||||
is_paused: *condition_common
|
||||
is_playing: *condition_common
|
||||
is_paused: *condition_common_with_for
|
||||
is_playing: *condition_common_with_for
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -19,6 +20,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::media_player::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::media_player::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Media player is off"
|
||||
@@ -37,6 +41,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::media_player::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::media_player::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Media player is paused"
|
||||
@@ -46,6 +53,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::media_player::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::media_player::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Media player is playing"
|
||||
|
||||
@@ -314,7 +314,7 @@ class LocalMediaView(http.HomeAssistantView):
|
||||
|
||||
async def head(
|
||||
self, request: web.Request, source_dir_id: str, location: str
|
||||
) -> web.Response:
|
||||
) -> None:
|
||||
"""Handle a HEAD request.
|
||||
|
||||
This is sent by some DLNA renderers, like Samsung ones, prior to sending
|
||||
@@ -322,9 +322,7 @@ class LocalMediaView(http.HomeAssistantView):
|
||||
|
||||
Check whether the location exists or not.
|
||||
"""
|
||||
media_path = await self._validate_media_path(source_dir_id, location)
|
||||
mime_type, _ = mimetypes.guess_type(str(media_path))
|
||||
return web.Response(content_type=mime_type)
|
||||
await self._validate_media_path(source_dir_id, location)
|
||||
|
||||
async def get(
|
||||
self, request: web.Request, source_dir_id: str, location: str
|
||||
|
||||
@@ -27,9 +27,11 @@ _MOISTURE_NUMERICAL_DOMAIN_SPECS = {
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_detected": make_entity_state_condition(_MOISTURE_BINARY_DOMAIN_SPECS, STATE_ON),
|
||||
"is_detected": make_entity_state_condition(
|
||||
_MOISTURE_BINARY_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_detected": make_entity_state_condition(
|
||||
_MOISTURE_BINARY_DOMAIN_SPECS, STATE_OFF
|
||||
_MOISTURE_BINARY_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
"is_value": make_entity_numerical_condition(
|
||||
_MOISTURE_NUMERICAL_DOMAIN_SPECS, PERCENTAGE
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.moisture_threshold_entity: &moisture_threshold_entity
|
||||
- domain: input_number
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least",
|
||||
@@ -12,6 +13,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::moisture::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::moisture::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Moisture is detected"
|
||||
@@ -21,6 +25,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::moisture::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::moisture::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Moisture is not detected"
|
||||
|
||||
@@ -15,8 +15,12 @@ _OCCUPANCY_DOMAIN_SPECS = {
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_OFF),
|
||||
"is_detected": make_entity_state_condition(
|
||||
_OCCUPANCY_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_detected": make_entity_state_condition(
|
||||
_OCCUPANCY_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::occupancy::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::occupancy::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Occupancy is detected"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::occupancy::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::occupancy::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Occupancy is not detected"
|
||||
|
||||
@@ -7,8 +7,10 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_home": make_entity_state_condition(DOMAIN, STATE_HOME),
|
||||
"is_not_home": make_entity_state_condition(DOMAIN, STATE_NOT_HOME),
|
||||
"is_home": make_entity_state_condition(DOMAIN, STATE_HOME, support_duration=True),
|
||||
"is_not_home": make_entity_state_condition(
|
||||
DOMAIN, STATE_NOT_HOME, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_home: *condition_common
|
||||
is_not_home: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::person::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::person::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Person is home"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::person::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::person::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Person is not home"
|
||||
|
||||
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from . import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::remote::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::remote::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Remote is off"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::remote::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::remote::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Remote is on"
|
||||
|
||||
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::schedule::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::schedule::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Schedule is off"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::schedule::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::schedule::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Schedule is on"
|
||||
|
||||
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from . import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::siren::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::siren::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Siren is off"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::siren::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::siren::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Siren is on"
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.const import ATTR_TIME
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
|
||||
from homeassistant.helpers import config_validation as cv, service
|
||||
from homeassistant.helpers.entity_platform import DATA_DOMAIN_PLATFORM_ENTITIES
|
||||
|
||||
from .const import ATTR_QUEUE_POSITION, DOMAIN
|
||||
from .media_player import SonosMediaPlayerEntity
|
||||
@@ -34,11 +35,25 @@ ATTR_WITH_GROUP = "with_group"
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Sonos services."""
|
||||
|
||||
async def async_handle_snapshot_restore(
|
||||
entities: list[SonosMediaPlayerEntity], service_call: ServiceCall
|
||||
) -> None:
|
||||
"""Handle snapshot and restore services."""
|
||||
speakers = [entity.speaker for entity in entities]
|
||||
@service.verify_domain_control(DOMAIN)
|
||||
async def async_service_handle(service_call: ServiceCall) -> None:
|
||||
"""Handle dispatched services."""
|
||||
platform_entities = hass.data.get(DATA_DOMAIN_PLATFORM_ENTITIES, {}).get(
|
||||
(MEDIA_PLAYER_DOMAIN, DOMAIN), {}
|
||||
)
|
||||
|
||||
entities = await service.async_extract_entities(
|
||||
platform_entities.values(), service_call
|
||||
)
|
||||
|
||||
if not entities:
|
||||
return
|
||||
|
||||
speakers: list[SonosSpeaker] = []
|
||||
for entity in entities:
|
||||
assert isinstance(entity, SonosMediaPlayerEntity)
|
||||
speakers.append(entity.speaker)
|
||||
|
||||
config_entry = speakers[0].config_entry # All speakers share the same entry
|
||||
|
||||
if service_call.service == SERVICE_SNAPSHOT:
|
||||
@@ -50,22 +65,16 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
hass, config_entry, speakers, service_call.data[ATTR_WITH_GROUP]
|
||||
)
|
||||
|
||||
service.async_register_batched_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_SNAPSHOT,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema={vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean},
|
||||
func=async_handle_snapshot_restore,
|
||||
join_unjoin_schema = cv.make_entity_service_schema(
|
||||
{vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean}
|
||||
)
|
||||
|
||||
service.async_register_batched_platform_entity_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_RESTORE,
|
||||
entity_domain=MEDIA_PLAYER_DOMAIN,
|
||||
schema={vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean},
|
||||
func=async_handle_snapshot_restore,
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema
|
||||
)
|
||||
|
||||
service.async_register_platform_entity_service(
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
snapshot:
|
||||
target:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
entity_id:
|
||||
selector:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
with_group:
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
restore:
|
||||
target:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
entity_id:
|
||||
selector:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
with_group:
|
||||
default: true
|
||||
selector:
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
"restore": {
|
||||
"description": "Restores a snapshot of a media player.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Name of entity that will be restored.",
|
||||
"name": "Entity"
|
||||
},
|
||||
"with_group": {
|
||||
"description": "Whether the group layout and the state of other speakers in the group should also be restored.",
|
||||
"name": "[%key:component::sonos::services::snapshot::fields::with_group::name%]"
|
||||
@@ -193,6 +197,10 @@
|
||||
"snapshot": {
|
||||
"description": "Takes a snapshot of a media player.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"description": "Name of entity that will be snapshot.",
|
||||
"name": "Entity"
|
||||
},
|
||||
"with_group": {
|
||||
"description": "Whether the snapshot should include the group layout and the state of other speakers in the group.",
|
||||
"name": "With group"
|
||||
|
||||
@@ -11,8 +11,12 @@ from .const import DOMAIN
|
||||
SWITCH_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_BOOLEAN_DOMAIN: DomainSpec()}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(SWITCH_DOMAIN_SPECS, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(SWITCH_DOMAIN_SPECS, STATE_ON),
|
||||
"is_off": make_entity_state_condition(
|
||||
SWITCH_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
),
|
||||
"is_on": make_entity_state_condition(
|
||||
SWITCH_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::switch::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::switch::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Switch is off"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::switch::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::switch::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Switch is on"
|
||||
|
||||
@@ -72,7 +72,6 @@ class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None])
|
||||
# and we actually have a way to connect to the device
|
||||
return (
|
||||
self.hass.state is CoreState.running
|
||||
and self.connectable
|
||||
and self.device.poll_needed(seconds_since_last_poll)
|
||||
and bool(
|
||||
bluetooth.async_ble_device_from_address(
|
||||
|
||||
@@ -121,15 +121,6 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"battery_range": {
|
||||
"default": "mdi:battery",
|
||||
"state": {
|
||||
"critical": "mdi:battery-alert-variant-outline",
|
||||
"high": "mdi:battery-80",
|
||||
"low": "mdi:battery-20",
|
||||
"medium": "mdi:battery-50"
|
||||
}
|
||||
},
|
||||
"light_level": {
|
||||
"default": "mdi:brightness-7",
|
||||
"state": {
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import switchbot
|
||||
from switchbot import HumidifierWaterLevel, SwitchbotModel
|
||||
from switchbot import HumidifierWaterLevel
|
||||
from switchbot.const.air_purifier import AirQualityLevel
|
||||
|
||||
from homeassistant.components.bluetooth import async_last_service_info
|
||||
@@ -38,16 +35,8 @@ from .entity import SwitchbotEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SwitchBotSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes SwitchBot sensor entities with optional value transformation."""
|
||||
|
||||
value_fn: Callable[[str | int | None], str | int | None] = lambda v: v
|
||||
|
||||
|
||||
SENSOR_TYPES: dict[str, SwitchBotSensorEntityDescription] = {
|
||||
"rssi": SwitchBotSensorEntityDescription(
|
||||
SENSOR_TYPES: dict[str, SensorEntityDescription] = {
|
||||
"rssi": SensorEntityDescription(
|
||||
key="rssi",
|
||||
translation_key="bluetooth_signal",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -56,7 +45,7 @@ SENSOR_TYPES: dict[str, SwitchBotSensorEntityDescription] = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"wifi_rssi": SwitchBotSensorEntityDescription(
|
||||
"wifi_rssi": SensorEntityDescription(
|
||||
key="wifi_rssi",
|
||||
translation_key="wifi_signal",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -65,91 +54,78 @@ SENSOR_TYPES: dict[str, SwitchBotSensorEntityDescription] = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"battery": SwitchBotSensorEntityDescription(
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"co2": SwitchBotSensorEntityDescription(
|
||||
"co2": SensorEntityDescription(
|
||||
key="co2",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
),
|
||||
"lightLevel": SwitchBotSensorEntityDescription(
|
||||
"lightLevel": SensorEntityDescription(
|
||||
key="lightLevel",
|
||||
translation_key="light_level",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"humidity": SwitchBotSensorEntityDescription(
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
),
|
||||
"illuminance": SwitchBotSensorEntityDescription(
|
||||
"illuminance": SensorEntityDescription(
|
||||
key="illuminance",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
),
|
||||
"temperature": SwitchBotSensorEntityDescription(
|
||||
"temperature": SensorEntityDescription(
|
||||
key="temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
"power": SwitchBotSensorEntityDescription(
|
||||
"power": SensorEntityDescription(
|
||||
key="power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
"current": SwitchBotSensorEntityDescription(
|
||||
"current": SensorEntityDescription(
|
||||
key="current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
),
|
||||
"voltage": SwitchBotSensorEntityDescription(
|
||||
"voltage": SensorEntityDescription(
|
||||
key="voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
),
|
||||
"aqi_level": SwitchBotSensorEntityDescription(
|
||||
"aqi_level": SensorEntityDescription(
|
||||
key="aqi_level",
|
||||
translation_key="aqi_quality_level",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[member.name.lower() for member in AirQualityLevel],
|
||||
),
|
||||
"energy": SwitchBotSensorEntityDescription(
|
||||
"energy": SensorEntityDescription(
|
||||
key="energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
"water_level": SwitchBotSensorEntityDescription(
|
||||
"water_level": SensorEntityDescription(
|
||||
key="water_level",
|
||||
translation_key="water_level",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=HumidifierWaterLevel.get_levels(),
|
||||
),
|
||||
"battery_range": SwitchBotSensorEntityDescription(
|
||||
key="battery_range",
|
||||
translation_key="battery_range",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["critical", "low", "medium", "high"],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda v: {
|
||||
"<10%": "critical",
|
||||
"10-19%": "low",
|
||||
"20-59%": "medium",
|
||||
">=60%": "high",
|
||||
}.get(str(v)),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +136,6 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up Switchbot sensor based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
parsed_data = coordinator.device.parsed_data
|
||||
sensor_entities: list[SensorEntity] = []
|
||||
if isinstance(coordinator.device, switchbot.SwitchbotRelaySwitch2PM):
|
||||
sensor_entities.extend(
|
||||
@@ -169,24 +144,10 @@ async def async_setup_entry(
|
||||
for sensor in coordinator.device.get_parsed_data(channel)
|
||||
if sensor in SENSOR_TYPES
|
||||
)
|
||||
elif coordinator.model == SwitchbotModel.PRESENCE_SENSOR:
|
||||
sensor_entities.extend(
|
||||
SwitchBotSensor(coordinator, sensor)
|
||||
for sensor in (
|
||||
*(
|
||||
s
|
||||
for s in parsed_data
|
||||
if s in SENSOR_TYPES and s not in ("battery", "battery_range")
|
||||
),
|
||||
"battery_range",
|
||||
)
|
||||
)
|
||||
if "battery" in parsed_data:
|
||||
sensor_entities.append(SwitchBotSensor(coordinator, "battery"))
|
||||
else:
|
||||
sensor_entities.extend(
|
||||
SwitchBotSensor(coordinator, sensor)
|
||||
for sensor in parsed_data
|
||||
for sensor in coordinator.device.parsed_data
|
||||
if sensor in SENSOR_TYPES
|
||||
)
|
||||
sensor_entities.append(SwitchbotRSSISensor(coordinator, "rssi"))
|
||||
@@ -196,8 +157,6 @@ async def async_setup_entry(
|
||||
class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
"""Representation of a Switchbot sensor."""
|
||||
|
||||
entity_description: SwitchBotSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SwitchbotDataUpdateCoordinator,
|
||||
@@ -226,7 +185,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> str | int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.parsed_data.get(self._sensor))
|
||||
return self.parsed_data[self._sensor]
|
||||
|
||||
|
||||
class SwitchbotRSSISensor(SwitchBotSensor):
|
||||
|
||||
@@ -291,15 +291,6 @@
|
||||
"unhealthy": "Unhealthy"
|
||||
}
|
||||
},
|
||||
"battery_range": {
|
||||
"name": "Battery range",
|
||||
"state": {
|
||||
"critical": "Critical",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]"
|
||||
}
|
||||
},
|
||||
"bluetooth_signal": {
|
||||
"name": "Bluetooth signal"
|
||||
},
|
||||
|
||||
@@ -6,9 +6,13 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from . import DOMAIN, STATUS_ACTIVE, STATUS_IDLE, STATUS_PAUSED
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_active": make_entity_state_condition(DOMAIN, STATUS_ACTIVE),
|
||||
"is_paused": make_entity_state_condition(DOMAIN, STATUS_PAUSED),
|
||||
"is_idle": make_entity_state_condition(DOMAIN, STATUS_IDLE),
|
||||
"is_active": make_entity_state_condition(
|
||||
DOMAIN, STATUS_ACTIVE, support_duration=True
|
||||
),
|
||||
"is_paused": make_entity_state_condition(
|
||||
DOMAIN, STATUS_PAUSED, support_duration=True
|
||||
),
|
||||
"is_idle": make_entity_state_condition(DOMAIN, STATUS_IDLE, support_duration=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_active: *condition_common
|
||||
is_paused: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if"
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least"
|
||||
},
|
||||
"conditions": {
|
||||
"is_active": {
|
||||
@@ -8,6 +9,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::timer::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::timer::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Timer is active"
|
||||
@@ -17,6 +21,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::timer::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::timer::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Timer is idle"
|
||||
@@ -26,6 +33,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::timer::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::timer::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Timer is paused"
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.helpers.condition import (
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"all_completed": make_entity_state_condition(DOMAIN, "0"),
|
||||
"all_completed": make_entity_state_condition(DOMAIN, "0", support_duration=True),
|
||||
"incomplete": make_entity_numerical_condition(DOMAIN),
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
.incomplete_threshold_entity: &incomplete_threshold_entity
|
||||
- domain: input_number
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"condition_threshold_name": "Threshold type"
|
||||
},
|
||||
"conditions": {
|
||||
@@ -9,6 +10,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::todo::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::todo::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "All to-do items completed"
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, SENSOR_UNIQUE_ID_MIGRATION
|
||||
from .coordinator import TwenteMilieuConfigEntry, TwenteMilieuDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CALENDAR, Platform.SENSOR]
|
||||
@@ -18,21 +14,6 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: TwenteMilieuConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Twente Milieu from a config entry."""
|
||||
old_prefix = f"{DOMAIN}_{entry.unique_id}_"
|
||||
|
||||
@callback
|
||||
def _migrate_unique_id(
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
if not entity_entry.unique_id.startswith(old_prefix):
|
||||
return None
|
||||
old_key = entity_entry.unique_id.removeprefix(old_prefix)
|
||||
if (new_key := SENSOR_UNIQUE_ID_MIGRATION.get(old_key)) is None:
|
||||
return None
|
||||
return {"new_unique_id": f"{entry.unique_id}_{new_key}"}
|
||||
|
||||
await er.async_migrate_entries(hass, entry.entry_id, _migrate_unique_id)
|
||||
|
||||
coordinator = TwenteMilieuDataUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -22,11 +22,3 @@ WASTE_TYPE_TO_DESCRIPTION = {
|
||||
WasteType.PAPER: "Paper waste pickup",
|
||||
WasteType.TREE: "Christmas tree pickup",
|
||||
}
|
||||
|
||||
SENSOR_UNIQUE_ID_MIGRATION = {
|
||||
"tree": "tree",
|
||||
"Non-recyclable": "non_recyclable",
|
||||
"Organic": "organic",
|
||||
"Paper": "paper",
|
||||
"Plastic": "packages",
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TwenteMilieuConfigEntry
|
||||
from .entity import TwenteMilieuEntity
|
||||
|
||||
@@ -34,25 +36,25 @@ SENSORS: tuple[TwenteMilieuSensorDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="non_recyclable",
|
||||
key="Non-recyclable",
|
||||
translation_key="non_recyclable_waste_pickup",
|
||||
waste_type=WasteType.NON_RECYCLABLE,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="organic",
|
||||
key="Organic",
|
||||
translation_key="organic_waste_pickup",
|
||||
waste_type=WasteType.ORGANIC,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="paper",
|
||||
key="Paper",
|
||||
translation_key="paper_waste_pickup",
|
||||
waste_type=WasteType.PAPER,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
TwenteMilieuSensorDescription(
|
||||
key="packages",
|
||||
key="Plastic",
|
||||
translation_key="packages_waste_pickup",
|
||||
waste_type=WasteType.PACKAGES,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
@@ -84,7 +86,7 @@ class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity):
|
||||
"""Initialize the Twente Milieu entity."""
|
||||
super().__init__(entry)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
|
||||
self._attr_unique_id = f"{DOMAIN}_{entry.data[CONF_ID]}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> date | None:
|
||||
|
||||
@@ -7,8 +7,12 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
|
||||
from .const import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_available": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
"is_not_available": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_available": make_entity_state_condition(
|
||||
DOMAIN, STATE_ON, support_duration=True
|
||||
),
|
||||
"is_not_available": make_entity_state_condition(
|
||||
DOMAIN, STATE_OFF, support_duration=True
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
selector:
|
||||
duration:
|
||||
|
||||
is_available: *condition_common
|
||||
is_not_available: *condition_common
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_name": "Condition passes if",
|
||||
"condition_for_name": "For at least",
|
||||
"trigger_behavior_name": "Trigger when",
|
||||
"trigger_for_name": "For at least"
|
||||
},
|
||||
@@ -10,6 +11,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::update::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::update::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Update is available"
|
||||
@@ -19,6 +23,9 @@
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::update::common::condition_behavior_name%]"
|
||||
},
|
||||
"for": {
|
||||
"name": "[%key:component::update::common::condition_for_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Update is not available"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user