Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions homeassistant/components/knx/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class FanZeroMode(StrEnum):
Platform.FAN,
Platform.DATETIME,
Platform.LIGHT,
Platform.SCENE,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,
Expand Down Expand Up @@ -227,3 +228,9 @@ class FanConf:
"""Common config keys for fan."""

MAX_STEP: Final = "max_step"


class SceneConf:
"""Common config keys for scene."""

SCENE_NUMBER: Final = "scene_number"
90 changes: 76 additions & 14 deletions homeassistant/components/knx/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,23 @@
from homeassistant.components.scene import BaseScene
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.typing import ConfigType

from .const import KNX_ADDRESS, KNX_MODULE_KEY
from .entity import KnxYamlEntity
from .const import DOMAIN, KNX_ADDRESS, KNX_MODULE_KEY, SceneConf
from .entity import (
KnxUiEntity,
KnxUiEntityPlatformController,
KnxYamlEntity,
_KnxEntityBase,
)
from .knx_module import KNXModule
from .schema import SceneSchema
from .storage.const import CONF_ENTITY, CONF_GA_SCENE
from .storage.util import ConfigExtractor


async def async_setup_entry(
Expand All @@ -26,18 +36,53 @@ async def async_setup_entry(
) -> None:
"""Set up scene(s) for KNX platform."""
knx_module = hass.data[KNX_MODULE_KEY]
config: list[ConfigType] = knx_module.config_yaml[Platform.SCENE]
platform = async_get_current_platform()
knx_module.config_store.add_platform(
platform=Platform.SCENE,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiScene,
),
)

async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config)
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := knx_module.config_yaml.get(Platform.SCENE):
entities.extend(
KnxYamlScene(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.data["entities"].get(Platform.SCENE):
entities.extend(
KnxUiScene(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
)
if entities:
async_add_entities(entities)


class KNXScene(KnxYamlEntity, BaseScene):
class _KnxScene(BaseScene, _KnxEntityBase):
"""Representation of a KNX scene."""

_device: XknxScene

async def _async_activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
await self._device.run()

def after_update_callback(self, device: XknxDevice) -> None:
"""Call after device was updated."""
self._async_record_activation()
super().after_update_callback(device)


class KnxYamlScene(_KnxScene, KnxYamlEntity):
"""Representation of a KNX scene configured from YAML."""

_device: XknxScene

def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
"""Init KNX scene."""
"""Initialize KNX scene."""
super().__init__(
knx_module=knx_module,
device=XknxScene(
Expand All @@ -52,11 +97,28 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
f"{self._device.scene_value.group_address}_{self._device.scene_number}"
)

async def _async_activate(self, **kwargs: Any) -> None:
"""Activate the scene."""
await self._device.run()

def after_update_callback(self, device: XknxDevice) -> None:
"""Call after device was updated."""
self._async_record_activation()
super().after_update_callback(device)
class KnxUiScene(_KnxScene, KnxUiEntity):
"""Representation of a KNX scene configured from the UI."""

_device: XknxScene

def __init__(
self,
knx_module: KNXModule,
unique_id: str,
config: ConfigType,
) -> None:
"""Initialize KNX scene."""
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
knx_conf = ConfigExtractor(config[DOMAIN])
self._device = XknxScene(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address=knx_conf.get_write(CONF_GA_SCENE),
scene_number=knx_conf.get(SceneConf.SCENE_NUMBER),
)
3 changes: 2 additions & 1 deletion homeassistant/components/knx/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
CoverConf,
FanConf,
FanZeroMode,
SceneConf,
)
from .validation import (
backwards_compatible_xknx_climate_enum_member,
Expand Down Expand Up @@ -822,7 +823,7 @@ class SceneSchema(KNXPlatformSchema):
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(KNX_ADDRESS): ga_list_validator,
vol.Required(CONF_SCENE_NUMBER): vol.All(
vol.Required(SceneConf.SCENE_NUMBER): vol.All(
vol.Coerce(int), vol.Range(min=1, max=64)
),
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/knx/storage/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,8 @@
CONF_GA_HUE: Final = "ga_hue"
CONF_GA_SATURATION: Final = "ga_saturation"

# Scene
CONF_GA_SCENE: Final = "ga_scene"

# Sensor
CONF_ALWAYS_CALLBACK: Final = "always_callback"
22 changes: 22 additions & 0 deletions homeassistant/components/knx/storage/entity_store_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
CoverConf,
FanConf,
FanZeroMode,
SceneConf,
)
from ..dpt import get_supported_dpts
from .const import (
Expand Down Expand Up @@ -82,6 +83,7 @@
CONF_GA_RED_BRIGHTNESS,
CONF_GA_RED_SWITCH,
CONF_GA_SATURATION,
CONF_GA_SCENE,
CONF_GA_SENSOR,
CONF_GA_SETPOINT_SHIFT,
CONF_GA_SPEED,
Expand Down Expand Up @@ -419,6 +421,25 @@ class LightColorMode(StrEnum):
),
)

SCENE_KNX_SCHEMA = vol.Schema(
{
vol.Required(CONF_GA_SCENE): GASelector(
state=False,
passive=False,
write_required=True,
valid_dpt=["17.001", "18.001"],
),
vol.Required(SceneConf.SCENE_NUMBER): AllSerializeFirst(
selector.NumberSelector(
selector.NumberSelectorConfig(
min=1, max=64, step=1, mode=selector.NumberSelectorMode.BOX
)
),
vol.Coerce(int),
),
},
)

SWITCH_KNX_SCHEMA = vol.Schema(
{
vol.Required(CONF_GA_SWITCH): GASelector(write_required=True, valid_dpt="1"),
Expand Down Expand Up @@ -694,6 +715,7 @@ def _validate_sensor_attributes(config: dict) -> dict:
Platform.DATETIME: DATETIME_KNX_SCHEMA,
Platform.FAN: FAN_KNX_SCHEMA,
Platform.LIGHT: LIGHT_KNX_SCHEMA,
Platform.SCENE: SCENE_KNX_SCHEMA,
Platform.SENSOR: SENSOR_KNX_SCHEMA,
Platform.SWITCH: SWITCH_KNX_SCHEMA,
Platform.TIME: TIME_KNX_SCHEMA,
Expand Down
13 changes: 13 additions & 0 deletions homeassistant/components/knx/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,19 @@
}
}
},
"scene": {
"description": "A KNX entity can activate a KNX scene and updates when the scene number is received.",
"knx": {
"ga_scene": {
"description": "Group address to activate a scene.",
"label": "Scene"
},
"scene_number": {
"description": "The scene number this entity is associated with.",
"label": "Scene number"
}
}
},
"sensor": {
"description": "Read-only entity for numeric or string datapoints. Temperature, percent etc.",
"knx": {
Expand Down
24 changes: 24 additions & 0 deletions tests/components/knx/fixtures/config_store_scene.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": 2,
"minor_version": 2,
"key": "knx/config_store.json",
"data": {
"entities": {
"scene": {
"knx_es_01KCXJ181N1TEDNC81WXEMXRNS": {
"entity": {
"name": "test",
"device_info": null,
"entity_category": null
},
"knx": {
"ga_scene": {
"write": "1/1/1"
},
"scene_number": 12
}
}
}
}
}
}
44 changes: 44 additions & 0 deletions tests/components/knx/snapshots/test_websocket.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -1576,6 +1576,50 @@
'type': 'result',
})
# ---
# name: test_knx_get_schema[scene]
dict({
'id': 1,
'result': list([
dict({
'name': 'ga_scene',
'options': dict({
'passive': False,
'state': False,
'validDPTs': list([
dict({
'main': 17,
'sub': 1,
}),
dict({
'main': 18,
'sub': 1,
}),
]),
'write': dict({
'required': True,
}),
}),
'required': True,
'type': 'knx_group_address',
}),
dict({
'name': 'scene_number',
'required': True,
'selector': dict({
'number': dict({
'max': 64.0,
'min': 1.0,
'mode': 'box',
'step': 1.0,
}),
}),
'type': 'ha_selector',
}),
]),
'success': True,
'type': 'result',
})
# ---
# name: test_knx_get_schema[sensor]
dict({
'id': 1,
Expand Down
40 changes: 39 additions & 1 deletion tests/components/knx/test_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

from homeassistant.components.knx.const import KNX_ADDRESS
from homeassistant.components.knx.schema import SceneSchema
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, EntityCategory
from homeassistant.const import (
CONF_ENTITY_CATEGORY,
CONF_NAME,
EntityCategory,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from . import KnxEntityGenerator
from .conftest import KNXTestKit

from tests.common import async_capture_events
Expand Down Expand Up @@ -56,3 +62,35 @@ async def test_activate_knx_scene(
# different scene number - should not be recorded
await knx.receive_write("1/1/1", (0x00,))
assert len(events) == 4


async def test_scene_ui_create(
hass: HomeAssistant,
knx: KNXTestKit,
create_ui_entity: KnxEntityGenerator,
) -> None:
"""Test creating a scene."""
await knx.setup_integration()
await create_ui_entity(
platform=Platform.SCENE,
entity_data={"name": "test"},
knx_data={
"ga_scene": {"write": "1/1/1"},
"scene_number": 5,
},
)
# activate scene from HA
await hass.services.async_call(
"scene", "turn_on", {"entity_id": "scene.test"}, blocking=True
)
await knx.assert_write("1/1/1", (0x04,)) # raw scene number is 0-based


async def test_scene_ui_load(hass: HomeAssistant, knx: KNXTestKit) -> None:
"""Test loading a scene from storage."""
await knx.setup_integration(config_store_fixture="config_store_scene.json")
# activate scene from HA
await hass.services.async_call(
"scene", "turn_on", {"entity_id": "scene.test"}, blocking=True
)
await knx.assert_write("1/1/1", (0x0B,))
Loading