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
2 changes: 1 addition & 1 deletion homeassistant/components/kiosker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .coordinator import KioskerConfigEntry, KioskerDataUpdateCoordinator

_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]


async def async_setup_entry(hass: HomeAssistant, entry: KioskerConfigEntry) -> bool:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/kiosker/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
POLL_INTERVAL = 15
DEFAULT_SSL = False
DEFAULT_SSL_VERIFY = False
REFRESH_DELAY = 0.5
5 changes: 5 additions & 0 deletions homeassistant/components/kiosker/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"last_motion": {
"default": "mdi:motion-sensor"
}
},
"switch": {
"disable_screensaver": {
"default": "mdi:power-sleep"
}
}
}
}
5 changes: 5 additions & 0 deletions homeassistant/components/kiosker/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
"last_motion": {
"name": "Last motion"
}
},
"switch": {
"disable_screensaver": {
"name": "Disable screensaver"
}
}
}
}
97 changes: 97 additions & 0 deletions homeassistant/components/kiosker/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Switch platform for Kiosker."""

import asyncio
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from kiosker import (
AuthenticationError,
BadRequestError,
ConnectionError,
IPAuthenticationError,
KioskerAPI,
TLSVerificationError,
)

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import KioskerConfigEntry
from .const import REFRESH_DELAY
from .coordinator import KioskerData
from .entity import KioskerEntity

PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class KioskerSwitchEntityDescription(SwitchEntityDescription):
"""Kiosker switch description."""

set_state_fn: Callable[[KioskerAPI, bool], None]
is_on_fn: Callable[[KioskerData], bool | None]


SWITCHES: tuple[KioskerSwitchEntityDescription, ...] = (
KioskerSwitchEntityDescription(
key="disableScreensaver",
translation_key="disable_screensaver",
set_state_fn=lambda api, disabled: api.screensaver_set_disabled_state(disabled),
is_on_fn=lambda x: x.screensaver.disabled if x.screensaver else None,
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: KioskerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Kiosker switches based on a config entry."""
coordinator = entry.runtime_data

async_add_entities(
KioskerSwitch(coordinator, description) for description in SWITCHES
)


class KioskerSwitch(KioskerEntity, SwitchEntity):
"""Representation of a Kiosker switch."""

entity_description: KioskerSwitchEntityDescription

@property
def is_on(self) -> bool | None:
"""Return true if the switch is on."""
return self.entity_description.is_on_fn(self.coordinator.data)

async def _handle_method_call(self, state: bool) -> None:
"""Handle method call with error handling."""
try:
await self.hass.async_add_executor_job(
self.entity_description.set_state_fn, self.coordinator.api, state
)
except AuthenticationError as exc:
raise HomeAssistantError("Authentication failed") from exc
except IPAuthenticationError as exc:
raise HomeAssistantError("IP Authentication failed") from exc
Comment thread
Claeysson marked this conversation as resolved.
except ConnectionError as exc:
Comment thread
Claeysson marked this conversation as resolved.
Comment thread
Claeysson marked this conversation as resolved.
Comment thread
Claeysson marked this conversation as resolved.
raise HomeAssistantError(f"Connection failed: {exc}") from exc
except TLSVerificationError as exc:
raise HomeAssistantError(f"TLS verification failed: {exc}") from exc
except BadRequestError as exc:
raise ServiceValidationError(f"Bad request: {exc}") from exc

await asyncio.sleep(REFRESH_DELAY)
await self.coordinator.async_refresh()

Comment thread
Claeysson marked this conversation as resolved.
Comment thread
Claeysson marked this conversation as resolved.
async def async_turn_on(self, **_kwargs: Any) -> None:
"""Turn the switch on."""
await self._handle_method_call(True)

async def async_turn_off(self, **_kwargs: Any) -> None:
"""Turn the switch off."""
await self._handle_method_call(False)
51 changes: 51 additions & 0 deletions tests/components/kiosker/snapshots/test_switch.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# serializer version: 1
# name: test_all_entities[switch.kiosker_a98be1ce_disable_screensaver-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.kiosker_a98be1ce_disable_screensaver',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Disable screensaver',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Disable screensaver',
'platform': 'kiosker',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'disable_screensaver',
'unique_id': 'A98BE1CE-5FE7-4A8D-B2C3-123456789ABC_disableScreensaver',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.kiosker_a98be1ce_disable_screensaver-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Kiosker A98BE1CE Disable screensaver',
}),
'context': <ANY>,
'entity_id': 'switch.kiosker_a98be1ce_disable_screensaver',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
203 changes: 203 additions & 0 deletions tests/components/kiosker/test_switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"""Test the Kiosker switch platform."""

from unittest.mock import MagicMock, patch

from kiosker import (
AuthenticationError,
BadRequestError,
Blackout,
ConnectionError,
IPAuthenticationError,
ScreensaverState,
TLSVerificationError,
)
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er

from . import setup_integration

from tests.common import MockConfigEntry, snapshot_platform

ENTITY_ID = "switch.kiosker_a98be1ce_disable_screensaver"


async def _setup_switch(
hass: HomeAssistant,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
*,
screensaver_disabled: bool = False,
) -> None:
mock_kiosker_api.screensaver_get_state.return_value = ScreensaverState(
visible=True, disabled=screensaver_disabled
)
mock_kiosker_api.blackout_get.return_value = Blackout(visible=False)
with patch("homeassistant.components.kiosker._PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry)


async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
mock_kiosker_api.screensaver_get_state.return_value = ScreensaverState(
visible=True, disabled=False
)
mock_kiosker_api.blackout_get.return_value = Blackout(visible=False)

with patch("homeassistant.components.kiosker._PLATFORMS", [Platform.SWITCH]):
await setup_integration(hass, mock_config_entry)

await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
Comment thread
Claeysson marked this conversation as resolved.


async def test_turn_on(
hass: HomeAssistant,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test turning the screensaver disable switch on."""
await _setup_switch(
hass, mock_kiosker_api, mock_config_entry, screensaver_disabled=False
)

mock_kiosker_api.screensaver_get_state.return_value = ScreensaverState(
visible=True, disabled=True
)
with patch("homeassistant.components.kiosker.switch.REFRESH_DELAY", 0):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

mock_kiosker_api.screensaver_set_disabled_state.assert_called_once_with(True)
assert hass.states.get(ENTITY_ID).state == "on"


async def test_turn_off(
hass: HomeAssistant,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test turning the screensaver disable switch off."""
await _setup_switch(
hass, mock_kiosker_api, mock_config_entry, screensaver_disabled=True
)

mock_kiosker_api.screensaver_get_state.return_value = ScreensaverState(
visible=True, disabled=False
)
with patch("homeassistant.components.kiosker.switch.REFRESH_DELAY", 0):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

mock_kiosker_api.screensaver_set_disabled_state.assert_called_once_with(False)
assert hass.states.get(ENTITY_ID).state == "off"


async def test_state_reflects_coordinator_data(
hass: HomeAssistant,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that state reflects coordinator data with no optimistic override."""
await _setup_switch(
hass, mock_kiosker_api, mock_config_entry, screensaver_disabled=False
)

# API still reports disabled=False (device hasn't updated yet)
with patch("homeassistant.components.kiosker.switch.REFRESH_DELAY", 0):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)

# Coordinator is the sole source of truth — reports off since device hasn't updated
assert hass.states.get(ENTITY_ID).state == "off"


@pytest.mark.parametrize(
("exception", "expected_exception"),
[
(AuthenticationError, HomeAssistantError),
(IPAuthenticationError, HomeAssistantError),
(ConnectionError, HomeAssistantError),
(TLSVerificationError, HomeAssistantError),
(BadRequestError, ServiceValidationError),
Comment thread
Claeysson marked this conversation as resolved.
],
)
async def test_turn_on_errors(
hass: HomeAssistant,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
exception: type[Exception],
expected_exception: type[Exception],
) -> None:
"""Test that API errors on turn_on are mapped to HA exceptions."""
await _setup_switch(hass, mock_kiosker_api, mock_config_entry)

mock_kiosker_api.screensaver_set_disabled_state.side_effect = exception("error")

with pytest.raises(expected_exception):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)


@pytest.mark.parametrize(
("exception", "expected_exception"),
[
(AuthenticationError, HomeAssistantError),
(IPAuthenticationError, HomeAssistantError),
(ConnectionError, HomeAssistantError),
(TLSVerificationError, HomeAssistantError),
(BadRequestError, ServiceValidationError),
Comment thread
Claeysson marked this conversation as resolved.
],
)
async def test_turn_off_errors(
hass: HomeAssistant,
mock_kiosker_api: MagicMock,
mock_config_entry: MockConfigEntry,
exception: type[Exception],
expected_exception: type[Exception],
) -> None:
"""Test that API errors on turn_off are mapped to HA exceptions."""
await _setup_switch(
hass, mock_kiosker_api, mock_config_entry, screensaver_disabled=True
)

mock_kiosker_api.screensaver_set_disabled_state.side_effect = exception("error")

with pytest.raises(expected_exception):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
Loading