diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 311dae20082..5a3d6078004 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py new file mode 100644 index 00000000000..7dfcc1e71ac --- /dev/null +++ b/homeassistant/components/fully_kiosk/switch.py @@ -0,0 +1,117 @@ +"""Fully Kiosk Browser switch.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from fullykiosk import FullyKiosk + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass +class FullySwitchEntityDescriptionMixin: + """Fully Kiosk Browser switch entity description mixin.""" + + on_action: Callable[[FullyKiosk], Any] + off_action: Callable[[FullyKiosk], Any] + is_on_fn: Callable[[dict[str, Any]], Any] + + +@dataclass +class FullySwitchEntityDescription( + SwitchEntityDescription, FullySwitchEntityDescriptionMixin +): + """Fully Kiosk Browser switch entity description.""" + + +SWITCHES: tuple[FullySwitchEntityDescription, ...] = ( + FullySwitchEntityDescription( + key="screensaver", + name="Screensaver", + on_action=lambda fully: fully.startScreensaver(), + off_action=lambda fully: fully.stopScreensaver(), + is_on_fn=lambda data: data.get("isInScreensaver"), + ), + FullySwitchEntityDescription( + key="maintenance", + name="Maintenance mode", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.enableLockedMode(), + off_action=lambda fully: fully.disableLockedMode(), + is_on_fn=lambda data: data.get("maintenanceMode"), + ), + FullySwitchEntityDescription( + key="kiosk", + name="Kiosk lock", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.lockKiosk(), + off_action=lambda fully: fully.unlockKiosk(), + is_on_fn=lambda data: data.get("kioskLocked"), + ), + FullySwitchEntityDescription( + key="motion-detection", + name="Motion detection", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.enableMotionDetection(), + off_action=lambda fully: fully.disableMotionDetection(), + is_on_fn=lambda data: data["settings"].get("motionDetection"), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser switch.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullySwitchEntity(coordinator, description) for description in SWITCHES + ) + + +class FullySwitchEntity(FullyKioskEntity, SwitchEntity): + """Fully Kiosk Browser switch entity.""" + + entity_description: FullySwitchEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullySwitchEntityDescription, + ) -> None: + """Initialize the Fully Kiosk Browser switch entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on.""" + if self.entity_description.is_on_fn(self.coordinator.data) is not None: + return bool(self.entity_description.is_on_fn(self.coordinator.data)) + return None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.on_action(self.coordinator.fully) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.off_action(self.coordinator.fully) + await self.coordinator.async_refresh() diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index 35d5c12e694..bed08b532fd 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -65,6 +65,9 @@ def mock_fully_kiosk() -> Generator[MagicMock, None, None]: client.getDeviceInfo.return_value = json.loads( load_fixture("deviceinfo.json", DOMAIN) ) + client.getSettings.return_value = json.loads( + load_fixture("listsettings.json", DOMAIN) + ) yield client diff --git a/tests/components/fully_kiosk/fixtures/listsettings.json b/tests/components/fully_kiosk/fixtures/listsettings.json new file mode 100644 index 00000000000..d11291032a9 --- /dev/null +++ b/tests/components/fully_kiosk/fixtures/listsettings.json @@ -0,0 +1,332 @@ +{ + "tapsToPinDialogInSingleAppMode": "7", + "mdmSystemUpdatePolicy": "0", + "showMenuHint": true, + "movementBeaconList": "", + "authUsername": "", + "motionSensitivity": "90", + "remoteAdminPassword": "admin_password", + "launchOnBoot": true, + "kioskAppBlacklist": "", + "folderCleanupTime": "", + "preventSleepWhileScreenOff": false, + "touchInteraction": true, + "showBackButton": true, + "volumeLimits": "", + "addressBarBgColor": -2236963, + "mdmSystemAppsToEnable": "", + "rebootTime": "", + "kioskWifiPin": "", + "appLauncherTextColor": -16777216, + "wifiSSID": "", + "keepSleepingIfUnplugged": false, + "microphoneAccess": false, + "barcodeScanBroadcastExtra": "", + "cacheMode": "-1", + "disableOutgoingCalls": false, + "knoxDisableUsbDebugging": false, + "screensaverBrightness": "0", + "thirdPartyCookies": true, + "desktopMode": false, + "protectedContent": false, + "knoxDisableBluetoothTethering": false, + "safeBrowsing": false, + "mqttBrokerPassword": "mqtt_password", + "screensaverOtherAppIntent": "", + "disablePowerButton": true, + "inUseWhileKeyboardVisible": false, + "screensaverFullscreen": true, + "actionBarTitle": "Fully Kiosk Browser", + "pageTransitions": false, + "urlBlacklist": "", + "appBlockReturnIntent": "", + "playAlarmSoundUntilPin": false, + "enableQrScan": false, + "knoxDisableBackup": false, + "appLauncherScaling": "100", + "showAppLauncherOnStart": false, + "appToRunInForegroundOnStart": "", + "usageStatistics": true, + "restartOnCrash": true, + "tabsFgColor": -16777216, + "knoxDisableGoogleAccountsAutoSync": false, + "appToRunOnStart": "", + "knoxDisableDeveloperMode": false, + "sleepOnPowerDisconnect": false, + "remotePdfFileMode": "0", + "keepOnWhileFullscreen": true, + "mainWebAutomation": "", + "mdmMinimumPasswordLength": "5", + "deleteHistoryOnReload": false, + "showQrScanButton": false, + "mdmApkToInstall": "", + "autoplayVideos": false, + "loadOverview": false, + "startURL": "https://homeassistant.local/", + "screensaverOtherApp": false, + "showNewTabButton": false, + "advancedKioskProtection": false, + "mdmDisableKeyguard": false, + "searchProviderUrl": "https://www.google.com/search?q=", + "defaultWebviewBackgroundColor": -1, + "motionDetectionAcoustic": false, + "reloadOnScreenOn": false, + "knoxDisableMultiUser": false, + "enableBackButton": true, + "mdmDisableUsbStorage": false, + "actionBarBgUrl": "", + "lockSafeMode": false, + "setWifiWakelock": false, + "knoxHideStatusBar": false, + "ignoreMotionWhenScreensaverOnOff": false, + "kioskWifiPinCustomIntent": "", + "showTabCloseButtons": true, + "knoxDisablePowerSavingMode": false, + "killOtherApps": false, + "knoxDisableMtp": false, + "deleteWebstorageOnReload": false, + "knoxDisablePowerOff": false, + "knoxDisableGoogleCrashReport": false, + "barcodeScanIntent": "", + "mdmAppsToDisable": "", + "barcodeScanTargetUrl": "", + "knoxDisablePowerButton": false, + "actionBarFgColor": -1, + "fadeInOutDuration": "200", + "audioRecordUploads": false, + "autoplayAudio": false, + "remoteAdminCamshot": true, + "showPrintButton": false, + "knoxDisableTaskManager": false, + "kioskExitGesture": "2", + "remoteAdminScreenshot": true, + "kioskAppWhitelist": "de.badaix.snapcast", + "enableVersionInfo": true, + "removeStatusBar": false, + "knoxDisableCellularData": false, + "showShareButton": false, + "recreateTabsOnReload": false, + "injectJsCode": "", + "screensaverWallpaperURL": "fully://color#000000", + "removeNavigationBar": false, + "forceScreenOrientation": "0", + "showStatusBar": false, + "knoxDisableWiFi": false, + "inUseWhileAnotherAppInForeground": false, + "appLauncherBackgroundColor": -1, + "mdmAppLockTaskWhitelist": "", + "webcamAccess": false, + "knoxDisableClipboard": false, + "playAlarmSoundOnMovement": false, + "loadContentZipFileUrl": "", + "forceImmersive": false, + "forceShowKeyboard": false, + "keepScreenOn": true, + "pauseMotionInBackground": false, + "showCamPreview": false, + "timeToScreenOffV2": "0", + "softKeyboard": true, + "statusBarColor": 0, + "mqttBrokerUsername": "mqtt", + "fileUploads": false, + "rootEnable": true, + "knoxDisableUsbTethering": false, + "screensaverPlaylist": "", + "showNavigationBar": false, + "launcherApps": "[\n {\n \"label\": \"Plex\",\n \"component\": \"com.plexapp.android\\/com.plexapp.plex.activities.SplashActivity\"\n },\n {\n \"label\": \"Spotify\",\n \"component\": \"com.spotify.music\\/com.spotify.music.MainActivity\"\n }\n]", + "autoImportSettings": true, + "motionDetection": false, + "cloudService": false, + "webviewDarkMode": "1", + "killAppsBeforeStartingList": "", + "websiteIntegration": true, + "clientCaPassword": "", + "deleteCookiesOnReload": false, + "errorURL": "", + "knoxDisableFactoryReset": false, + "knoxDisableCamera": false, + "knoxEnabled": false, + "playMedia": true, + "stopScreensaverOnMotion": true, + "redirectBlocked": false, + "mdmDisableADB": true, + "showTabs": false, + "restartAfterUpdate": true, + "mqttEventTopic": "$appId/event/$event/$deviceId", + "fontSize": "100", + "clearCacheEach": false, + "mdmDisableVolumeButtons": false, + "knoxDisableVpn": false, + "confirmExit": true, + "ignoreSSLerrors": true, + "mqttClientId": "", + "setRemoveSystemUI": false, + "wifiKey": "", + "screenOffInDarkness": false, + "knoxDisableWifiDirect": false, + "inactiveTabsBgColor": -4144960, + "useWideViewport": true, + "forceSwipeUnlock": false, + "geoLocationAccess": false, + "graphicsAccelerationMode": "2", + "barcodeScanBroadcastAction": "", + "authPassword": "", + "tabsBgColor": -2236963, + "loadCurrentPageOnReload": false, + "skipReloadIfStartUrlShowing": false, + "enablePullToRefresh": false, + "motionSensitivityAcoustic": "90", + "wifiMode": "0", + "ignoreMotionWhenMoving": false, + "knoxDisableAirViewMode": false, + "actionBarCustomButtonUrl": "", + "kioskHomeStartURL": true, + "environmentSensorsEnabled": true, + "mdmRuntimePermissionPolicy": "0", + "disableCamera": false, + "knoxDisableRecentTaskButton": false, + "knoxDisableAirCommandMode": false, + "remoteAdminSingleAppExit": false, + "mqttDeviceInfoTopic": "$appId/deviceInfo/$deviceId", + "darknessLevel": "10", + "actionBarInSettings": false, + "remoteAdminLan": true, + "knoxDisableSDCardWrite": false, + "movementBeaconDistance": "5", + "forceSleepIfUnplugged": false, + "swipeTabs": false, + "alarmSoundFileUrl": "", + "knoxDisableStatusBar": false, + "reloadOnInternet": false, + "knoxDisableNonMarketApps": false, + "knoxDisableSettingsChanges": false, + "barcodeScanListenKeys": false, + "mdmLockTask": false, + "timeToScreensaverV2": "900", + "movementDetection": true, + "clientCaUrl": "", + "sleepOnPowerConnect": false, + "resumeVideoAudio": true, + "addXffHeader": false, + "readNfcTag": false, + "swipeNavigation": false, + "knoxDisableVolumeButtons": false, + "screenOnOnMovement": true, + "knoxHideNavigationBar": false, + "timeToClearSingleAppData": "0", + "timeToGoBackground": "0", + "timeToShutdownOnPowerDisconnect": "0", + "webviewDragging": true, + "timeToRegainFocus": "600", + "pauseWebviewOnPause": false, + "knoxDisableUsbHostStorage": false, + "folderCleanupList": "", + "movementWhenUnplugged": false, + "actionBarBgColor": -15906911, + "enableZoom": false, + "resetZoomEach": false, + "knoxDisableAndroidBeam": false, + "reloadOnWifiOn": true, + "compassSensitivity": "50", + "touchesOtherAppsBreakIdle": true, + "reloadPageFailure": "300", + "knoxDisableBackButton": false, + "actionBarIconUrl": "", + "initialScale": "0", + "knoxDisableMicrophoneState": false, + "remoteAdminFileManagement": true, + "knoxDisableFirmwareRecovery": false, + "errorUrlOnDisconnection": "0", + "addRefererHeader": true, + "showHomeButton": true, + "reloadEachSeconds": "0", + "accelerometerSensitivity": "80", + "knoxDisableScreenCapture": false, + "knoxDisableWifiTethering": false, + "knoxDisableEdgeScreen": false, + "forceScreenOrientationGlobal": false, + "mqttBrokerUrl": "tcp://192.168.1.2:1883", + "wakeupOnPowerConnect": false, + "knoxDisableClipboardShare": false, + "forceScreenUnlock": true, + "knoxDisableAirplaneMode": false, + "barcodeScanInsertInputField": false, + "waitInternetOnReload": true, + "forceOpenByAppUrl": "", + "disableVolumeButtons": true, + "disableStatusBar": true, + "kioskPin": "1234", + "screensaverDaydream": false, + "detectIBeacons": false, + "kioskWifiPinAction": "0", + "stopScreensaverOnMovement": true, + "videoCaptureUploads": false, + "disableIncomingCalls": false, + "motionCameraId": "", + "knoxDisableSafeMode": false, + "webHostFilter": false, + "urlWhitelist": "", + "disableOtherApps": true, + "progressBarColor": -13611010, + "enableTapSound": true, + "volumeLicenseKey": "", + "showActionBar": false, + "ignoreJustOnceLauncher": false, + "remoteAdmin": true, + "launcherBgUrl": "", + "mdmDisableSafeModeBoot": true, + "launcherInjectCode": "", + "cameraCaptureUploads": false, + "disableNotifications": false, + "sleepSchedule": "", + "knoxDisableAudioRecord": false, + "setCpuWakelock": false, + "enablePopups": false, + "mqttEnabled": true, + "knoxDisableOtaUpgrades": false, + "mdmDisableStatusBar": false, + "lastVersionInfo": "1.39", + "textSelection": false, + "jsAlerts": true, + "knoxActiveByKiosk": false, + "disableHomeButton": true, + "webviewDebugging": true, + "mdmDisableAppsFromUnknownSources": true, + "knoxDisableBluetooth": false, + "showAddressBar": false, + "batteryWarning": "20", + "playerCacheImages": true, + "localPdfFileMode": "0", + "resendFormData": false, + "mdmDisableScreenCapture": false, + "kioskMode": true, + "singleAppMode": false, + "mdmPasswordQuality": "0", + "enableUrlOtherApps": true, + "navigationBarColor": 0, + "isRunning": true, + "knoxDisableHomeButton": false, + "webviewScrolling": true, + "motionFps": "5", + "enableLocalhost": false, + "reloadOnScreensaverStop": false, + "customUserAgent": "", + "screenBrightness": "9", + "knoxDisableVideoRecord": false, + "showProgressBar": true, + "formAutoComplete": true, + "showRefreshButton": false, + "knoxDisableHeadphoneState": false, + "showForwardButton": true, + "timeToClearLauncherAppData": "0", + "bluetoothMode": "0", + "enableFullscreenVideos": true, + "remoteFileMode": "0", + "barcodeScanSubmitInputField": false, + "knoxDisableMultiWindowMode": false, + "singleAppIntent": "", + "userAgent": "0", + "runInForeground": true, + "deleteCacheOnReload": false, + "screenOnOnMotion": true +} diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py new file mode 100644 index 00000000000..6b6d2829790 --- /dev/null +++ b/tests/components/fully_kiosk/test_switch.py @@ -0,0 +1,81 @@ +"""Test the Fully Kiosk Browser switches.""" +from unittest.mock import MagicMock + +from homeassistant.components.fully_kiosk.const import DOMAIN +import homeassistant.components.switch as switch +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_switches( + hass: HomeAssistant, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test Fully Kiosk switches.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + entity = hass.states.get("switch.amazon_fire_screensaver") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_screensaver") + assert entry + assert entry.unique_id == "abcdef-123456-screensaver" + await call_service(hass, "turn_on", "switch.amazon_fire_screensaver") + assert len(mock_fully_kiosk.startScreensaver.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_screensaver") + assert len(mock_fully_kiosk.stopScreensaver.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_maintenance_mode") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_maintenance_mode") + assert entry + assert entry.unique_id == "abcdef-123456-maintenance" + await call_service(hass, "turn_on", "switch.amazon_fire_maintenance_mode") + assert len(mock_fully_kiosk.enableLockedMode.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_maintenance_mode") + assert len(mock_fully_kiosk.disableLockedMode.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_kiosk_lock") + assert entity + assert entity.state == "on" + entry = entity_registry.async_get("switch.amazon_fire_kiosk_lock") + assert entry + assert entry.unique_id == "abcdef-123456-kiosk" + await call_service(hass, "turn_off", "switch.amazon_fire_kiosk_lock") + assert len(mock_fully_kiosk.unlockKiosk.mock_calls) == 1 + await call_service(hass, "turn_on", "switch.amazon_fire_kiosk_lock") + assert len(mock_fully_kiosk.lockKiosk.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_motion_detection") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_motion_detection") + assert entry + assert entry.unique_id == "abcdef-123456-motion-detection" + await call_service(hass, "turn_on", "switch.amazon_fire_motion_detection") + assert len(mock_fully_kiosk.enableMotionDetection.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_motion_detection") + assert len(mock_fully_kiosk.disableMotionDetection.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def call_service(hass, service, entity_id): + """Call any service on entity.""" + return hass.services.async_call( + switch.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True + )