-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add Kiosker switch platform #168858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
joostlek
merged 11 commits into
home-assistant:dev
from
Claeysson:kiosker-feature-switch
May 11, 2026
+363
−1
Merged
Add Kiosker switch platform #168858
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
145cdba
Add switch platform
Claeysson 7e739e0
Test for switch
Claeysson 2426a6a
Fixed p
Claeysson 49f7e3c
Fixed optimistic state
Claeysson 2bffba2
Added code coverage
Claeysson 955f3e7
added state lamba
Claeysson 5839e76
Removed optimistic state and fixed tests
Claeysson e5dcdb0
Removed unused logging
Claeysson c61a5c9
Removed future annotations
Claeysson acc0f6c
Moved delay time to const.py
Claeysson f835dc7
Rmeoved feature annotations
Claeysson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,3 +10,4 @@ | |
| POLL_INTERVAL = 15 | ||
| DEFAULT_SSL = False | ||
| DEFAULT_SSL_VERIFY = False | ||
| REFRESH_DELAY = 0.5 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| except ConnectionError as exc: | ||
|
Claeysson marked this conversation as resolved.
Claeysson marked this conversation as resolved.
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() | ||
|
|
||
|
Claeysson marked this conversation as resolved.
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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| }) | ||
| # --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
|
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), | ||
|
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), | ||
|
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, | ||
| ) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.