Add Fully Kiosk Browser integration with initial binary sensor platform (#76737)
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
parent
73ad34244e
commit
1e9ede25ad
21 changed files with 762 additions and 0 deletions
|
@ -108,6 +108,7 @@ homeassistant.components.fritzbox_callmonitor.*
|
||||||
homeassistant.components.fronius.*
|
homeassistant.components.fronius.*
|
||||||
homeassistant.components.frontend.*
|
homeassistant.components.frontend.*
|
||||||
homeassistant.components.fritz.*
|
homeassistant.components.fritz.*
|
||||||
|
homeassistant.components.fully_kiosk.*
|
||||||
homeassistant.components.geo_location.*
|
homeassistant.components.geo_location.*
|
||||||
homeassistant.components.geocaching.*
|
homeassistant.components.geocaching.*
|
||||||
homeassistant.components.gios.*
|
homeassistant.components.gios.*
|
||||||
|
|
|
@ -375,6 +375,8 @@ build.json @home-assistant/supervisor
|
||||||
/homeassistant/components/frontend/ @home-assistant/frontend
|
/homeassistant/components/frontend/ @home-assistant/frontend
|
||||||
/tests/components/frontend/ @home-assistant/frontend
|
/tests/components/frontend/ @home-assistant/frontend
|
||||||
/homeassistant/components/frontier_silicon/ @wlcrs
|
/homeassistant/components/frontier_silicon/ @wlcrs
|
||||||
|
/homeassistant/components/fully_kiosk/ @cgarwood
|
||||||
|
/tests/components/fully_kiosk/ @cgarwood
|
||||||
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
|
||||||
/tests/components/garages_amsterdam/ @klaasnicolaas
|
/tests/components/garages_amsterdam/ @klaasnicolaas
|
||||||
/homeassistant/components/gdacs/ @exxamalte
|
/homeassistant/components/gdacs/ @exxamalte
|
||||||
|
|
31
homeassistant/components/fully_kiosk/__init__.py
Normal file
31
homeassistant/components/fully_kiosk/__init__.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
"""The Fully Kiosk Browser integration."""
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import FullyKioskDataUpdateCoordinator
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Fully Kiosk Browser from a config entry."""
|
||||||
|
|
||||||
|
coordinator = FullyKioskDataUpdateCoordinator(hass, entry)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
73
homeassistant/components/fully_kiosk/binary_sensor.py
Normal file
73
homeassistant/components/fully_kiosk/binary_sensor.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
"""Fully Kiosk Browser sensor."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
BinarySensorEntity,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
SENSORS: tuple[BinarySensorEntityDescription, ...] = (
|
||||||
|
BinarySensorEntityDescription(
|
||||||
|
key="kioskMode",
|
||||||
|
name="Kiosk mode",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
BinarySensorEntityDescription(
|
||||||
|
key="plugged",
|
||||||
|
name="Plugged in",
|
||||||
|
device_class=BinarySensorDeviceClass.PLUG,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
BinarySensorEntityDescription(
|
||||||
|
key="isDeviceAdmin",
|
||||||
|
name="Device admin",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the Fully Kiosk Browser sensor."""
|
||||||
|
coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][
|
||||||
|
config_entry.entry_id
|
||||||
|
]
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
FullyBinarySensor(coordinator, description)
|
||||||
|
for description in SENSORS
|
||||||
|
if description.key in coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FullyBinarySensor(FullyKioskEntity, BinarySensorEntity):
|
||||||
|
"""Representation of a Fully Kiosk Browser binary sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: FullyKioskDataUpdateCoordinator,
|
||||||
|
description: BinarySensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the binary sensor."""
|
||||||
|
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 if the binary sensor is on."""
|
||||||
|
if (value := self.coordinator.data.get(self.entity_description.key)) is None:
|
||||||
|
return None
|
||||||
|
return bool(value)
|
63
homeassistant/components/fully_kiosk/config_flow.py
Normal file
63
homeassistant/components/fully_kiosk/config_flow.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
"""Config flow for Fully Kiosk Browser integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
from async_timeout import timeout
|
||||||
|
from fullykiosk import FullyKiosk
|
||||||
|
from fullykiosk.exceptions import FullyKioskError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||||
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DEFAULT_PORT, DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Fully Kiosk Browser."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> FlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
fully = FullyKiosk(
|
||||||
|
async_get_clientsession(self.hass),
|
||||||
|
user_input[CONF_HOST],
|
||||||
|
DEFAULT_PORT,
|
||||||
|
user_input[CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with timeout(15):
|
||||||
|
device_info = await fully.getDeviceInfo()
|
||||||
|
except (ClientConnectorError, FullyKioskError, asyncio.TimeoutError):
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(device_info["deviceID"])
|
||||||
|
self._abort_if_unique_id_configured(updates=user_input)
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=device_info["deviceName"], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
13
homeassistant/components/fully_kiosk/const.py
Normal file
13
homeassistant/components/fully_kiosk/const.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
"""Constants for the Fully Kiosk Browser integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
DOMAIN: Final = "fully_kiosk"
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
|
DEFAULT_PORT = 2323
|
48
homeassistant/components/fully_kiosk/coordinator.py
Normal file
48
homeassistant/components/fully_kiosk/coordinator.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
"""Provides the The Fully Kiosk Browser DataUpdateCoordinator."""
|
||||||
|
import asyncio
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
|
from async_timeout import timeout
|
||||||
|
from fullykiosk import FullyKiosk
|
||||||
|
from fullykiosk.exceptions import FullyKioskError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DEFAULT_PORT, LOGGER, UPDATE_INTERVAL
|
||||||
|
|
||||||
|
|
||||||
|
class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Define an object to hold Fully Kiosk Browser data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
self.fully = FullyKiosk(
|
||||||
|
async_get_clientsession(hass),
|
||||||
|
entry.data[CONF_HOST],
|
||||||
|
DEFAULT_PORT,
|
||||||
|
entry.data[CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
LOGGER,
|
||||||
|
name=entry.data[CONF_HOST],
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
|
"""Update data via library."""
|
||||||
|
try:
|
||||||
|
async with timeout(15):
|
||||||
|
# Get device info and settings in parallel
|
||||||
|
result = await asyncio.gather(
|
||||||
|
self.fully.getDeviceInfo(), self.fully.getSettings()
|
||||||
|
)
|
||||||
|
# Store settings under settings key in data
|
||||||
|
result[0]["settings"] = result[1]
|
||||||
|
return cast(dict[str, Any], result[0])
|
||||||
|
except FullyKioskError as error:
|
||||||
|
raise UpdateFailed(error) from error
|
26
homeassistant/components/fully_kiosk/entity.py
Normal file
26
homeassistant/components/fully_kiosk/entity.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
"""Base entity for the Fully Kiosk Browser integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import FullyKioskDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entity):
|
||||||
|
"""Defines a Fully Kiosk Browser entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the Fully Kiosk Browser entity."""
|
||||||
|
super().__init__(coordinator=coordinator)
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, coordinator.data["deviceID"])},
|
||||||
|
name=coordinator.data["deviceName"],
|
||||||
|
manufacturer=coordinator.data["deviceManufacturer"],
|
||||||
|
model=coordinator.data["deviceModel"],
|
||||||
|
sw_version=coordinator.data["appVersionName"],
|
||||||
|
configuration_url=f"http://{coordinator.data['ip4']}:2323",
|
||||||
|
)
|
10
homeassistant/components/fully_kiosk/manifest.json
Normal file
10
homeassistant/components/fully_kiosk/manifest.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"domain": "fully_kiosk",
|
||||||
|
"name": "Fully Kiosk Browser",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/fullykiosk",
|
||||||
|
"requirements": ["python-fullykiosk==0.0.11"],
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": ["@cgarwood"],
|
||||||
|
"iot_class": "local_polling"
|
||||||
|
}
|
20
homeassistant/components/fully_kiosk/strings.json
Normal file
20
homeassistant/components/fully_kiosk/strings.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
homeassistant/components/fully_kiosk/translations/en.json
Normal file
20
homeassistant/components/fully_kiosk/translations/en.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Account is already configured"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"invalid_auth": "Invalid authentication",
|
||||||
|
"unknown": "Unexpected error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"password": "Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,6 +125,7 @@ FLOWS = {
|
||||||
"fritzbox",
|
"fritzbox",
|
||||||
"fritzbox_callmonitor",
|
"fritzbox_callmonitor",
|
||||||
"fronius",
|
"fronius",
|
||||||
|
"fully_kiosk",
|
||||||
"garages_amsterdam",
|
"garages_amsterdam",
|
||||||
"gdacs",
|
"gdacs",
|
||||||
"generic",
|
"generic",
|
||||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -839,6 +839,16 @@ disallow_untyped_defs = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.fully_kiosk.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.geo_location.*]
|
[mypy-homeassistant.components.geo_location.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -1914,6 +1914,9 @@ python-family-hub-local==0.0.2
|
||||||
# homeassistant.components.darksky
|
# homeassistant.components.darksky
|
||||||
python-forecastio==1.4.0
|
python-forecastio==1.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.fully_kiosk
|
||||||
|
python-fullykiosk==0.0.11
|
||||||
|
|
||||||
# homeassistant.components.sms
|
# homeassistant.components.sms
|
||||||
# python-gammu==3.2.4
|
# python-gammu==3.2.4
|
||||||
|
|
||||||
|
|
|
@ -1313,6 +1313,9 @@ python-ecobee-api==0.2.14
|
||||||
# homeassistant.components.darksky
|
# homeassistant.components.darksky
|
||||||
python-forecastio==1.4.0
|
python-forecastio==1.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.fully_kiosk
|
||||||
|
python-fullykiosk==0.0.11
|
||||||
|
|
||||||
# homeassistant.components.homewizard
|
# homeassistant.components.homewizard
|
||||||
python-homewizard-energy==1.1.0
|
python-homewizard-energy==1.1.0
|
||||||
|
|
||||||
|
|
1
tests/components/fully_kiosk/__init__.py
Normal file
1
tests/components/fully_kiosk/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Fully Kiosk Browser integration."""
|
76
tests/components/fully_kiosk/conftest.py
Normal file
76
tests/components/fully_kiosk/conftest.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"""Fixtures for the Fully Kiosk Browser integration tests."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.fully_kiosk.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
title="Test device",
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_HOST: "127.0.0.1", CONF_PASSWORD: "mocked-password"},
|
||||||
|
unique_id="12345",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||||
|
"""Mock setting up a config entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.fully_kiosk.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup:
|
||||||
|
yield mock_setup
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]:
|
||||||
|
"""Return a mocked Fully Kiosk client for the config flow."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.fully_kiosk.config_flow.FullyKiosk",
|
||||||
|
autospec=True,
|
||||||
|
) as client_mock:
|
||||||
|
client = client_mock.return_value
|
||||||
|
client.getDeviceInfo.return_value = {
|
||||||
|
"deviceName": "Test device",
|
||||||
|
"deviceID": "12345",
|
||||||
|
}
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_fully_kiosk() -> Generator[MagicMock, None, None]:
|
||||||
|
"""Return a mocked Fully Kiosk client."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.fully_kiosk.coordinator.FullyKiosk",
|
||||||
|
autospec=True,
|
||||||
|
) as client_mock:
|
||||||
|
client = client_mock.return_value
|
||||||
|
client.getDeviceInfo.return_value = json.loads(
|
||||||
|
load_fixture("deviceinfo.json", DOMAIN)
|
||||||
|
)
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_fully_kiosk: MagicMock
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the Fully Kiosk Browser integration for testing."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return mock_config_entry
|
79
tests/components/fully_kiosk/fixtures/deviceinfo.json
Normal file
79
tests/components/fully_kiosk/fixtures/deviceinfo.json
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"deviceName": "Amazon Fire",
|
||||||
|
"batteryLevel": 100,
|
||||||
|
"isPlugged": true,
|
||||||
|
"SSID": "\"freewifi\"",
|
||||||
|
"Mac": "aa:bb:cc:dd:ee:ff",
|
||||||
|
"ip4": "192.168.1.234",
|
||||||
|
"ip6": "FE80::1874:2EFF:FEA2:7848",
|
||||||
|
"hostname4": "192.168.1.234",
|
||||||
|
"hostname6": "fe80::1874:2eff:fea2:7848%p2p0",
|
||||||
|
"wifiSignalLevel": 7,
|
||||||
|
"isMobileDataEnabled": true,
|
||||||
|
"screenOrientation": 90,
|
||||||
|
"screenBrightness": 9,
|
||||||
|
"screenLocked": false,
|
||||||
|
"screenOn": true,
|
||||||
|
"batteryTemperature": 27,
|
||||||
|
"plugged": true,
|
||||||
|
"keyguardLocked": false,
|
||||||
|
"locale": "en_US",
|
||||||
|
"serial": "ABCDEF1234567890",
|
||||||
|
"build": "cm_douglas-userdebug 5.1.1 LMY49M 731a881f9d test-keys",
|
||||||
|
"androidVersion": "5.1.1",
|
||||||
|
"webviewUA": "Mozilla/5.0 (Linux; Android 5.1.1; KFDOWI Build/LMY49M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Safari/537.36",
|
||||||
|
"motionDetectorStatus": 0,
|
||||||
|
"isDeviceAdmin": true,
|
||||||
|
"isDeviceOwner": false,
|
||||||
|
"internalStorageFreeSpace": 11675512832,
|
||||||
|
"internalStorageTotalSpace": 12938534912,
|
||||||
|
"ramUsedMemory": 1077755904,
|
||||||
|
"ramFreeMemory": 362373120,
|
||||||
|
"ramTotalMemory": 1440129024,
|
||||||
|
"appUsedMemory": 24720592,
|
||||||
|
"appFreeMemory": 59165440,
|
||||||
|
"appTotalMemory": 83886080,
|
||||||
|
"displayHeightPixels": 800,
|
||||||
|
"displayWidthPixels": 1280,
|
||||||
|
"isMenuOpen": false,
|
||||||
|
"topFragmentTag": "",
|
||||||
|
"isInDaydream": false,
|
||||||
|
"isRooted": true,
|
||||||
|
"isLicensed": true,
|
||||||
|
"isInScreensaver": false,
|
||||||
|
"kioskLocked": true,
|
||||||
|
"isInForcedSleep": false,
|
||||||
|
"maintenanceMode": false,
|
||||||
|
"kioskMode": true,
|
||||||
|
"startUrl": "https://homeassistant.local",
|
||||||
|
"currentTabIndex": 0,
|
||||||
|
"mqttConnected": true,
|
||||||
|
"deviceID": "abcdef-123456",
|
||||||
|
"appVersionCode": 875,
|
||||||
|
"appVersionName": "1.42.5",
|
||||||
|
"androidSdk": 22,
|
||||||
|
"deviceModel": "KFDOWI",
|
||||||
|
"deviceManufacturer": "amzn",
|
||||||
|
"foregroundApp": "de.ozerov.fully",
|
||||||
|
"currentPage": "https://homeassistant.local",
|
||||||
|
"lastAppStart": "8/13/2022 1:00:47 AM",
|
||||||
|
"sensorInfo": [
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"name": "PROXIMITY",
|
||||||
|
"vendor": "MTK",
|
||||||
|
"version": 1,
|
||||||
|
"accuracy": -1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 5,
|
||||||
|
"name": "LIGHT",
|
||||||
|
"vendor": "MTK",
|
||||||
|
"version": 1,
|
||||||
|
"accuracy": 3,
|
||||||
|
"values": [0, 0, 0],
|
||||||
|
"lastValuesTime": 1660435566561,
|
||||||
|
"lastAccuracyTime": 1660366847543
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
93
tests/components/fully_kiosk/test_binary_sensor.py
Normal file
93
tests/components/fully_kiosk/test_binary_sensor.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
"""Test the Fully Kiosk Browser binary sensors."""
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from fullykiosk import FullyKioskError
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
|
from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
|
ATTR_FRIENDLY_NAME,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
from homeassistant.util import dt
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_binary_sensors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_fully_kiosk: MagicMock,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test standard Fully Kiosk binary sensors."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.amazon_fire_plugged_in")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Plugged in"
|
||||||
|
|
||||||
|
entry = entity_registry.async_get("binary_sensor.amazon_fire_plugged_in")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "abcdef-123456-plugged"
|
||||||
|
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.amazon_fire_kiosk_mode")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Kiosk mode"
|
||||||
|
|
||||||
|
entry = entity_registry.async_get("binary_sensor.amazon_fire_kiosk_mode")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "abcdef-123456-kioskMode"
|
||||||
|
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.amazon_fire_device_admin")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
|
||||||
|
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Device admin"
|
||||||
|
|
||||||
|
entry = entity_registry.async_get("binary_sensor.amazon_fire_device_admin")
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "abcdef-123456-isDeviceAdmin"
|
||||||
|
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Test unknown/missing data
|
||||||
|
mock_fully_kiosk.getDeviceInfo.return_value = {}
|
||||||
|
async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.amazon_fire_plugged_in")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
# Test failed update
|
||||||
|
mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status")
|
||||||
|
async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("binary_sensor.amazon_fire_plugged_in")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
136
tests/components/fully_kiosk/test_config_flow.py
Normal file
136
tests/components/fully_kiosk/test_config_flow.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
"""Test the Fully Kiosk Browser config flow."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, Mock
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
from fullykiosk import FullyKioskError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.fully_kiosk.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_fully_kiosk_config_flow: MagicMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the full user initiated config flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result.get("type") == FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == SOURCE_USER
|
||||||
|
assert "flow_id" in result
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("type") == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2.get("title") == "Test device"
|
||||||
|
assert result2.get("data") == {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
}
|
||||||
|
assert "result" in result2
|
||||||
|
assert result2["result"].unique_id == "12345"
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"side_effect,reason",
|
||||||
|
[
|
||||||
|
(FullyKioskError("error", "status"), "cannot_connect"),
|
||||||
|
(ClientConnectorError(None, Mock()), "cannot_connect"),
|
||||||
|
(asyncio.TimeoutError, "cannot_connect"),
|
||||||
|
(RuntimeError, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_fully_kiosk_config_flow: MagicMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
side_effect: Exception,
|
||||||
|
reason: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test errors raised during flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert "flow_id" in result
|
||||||
|
flow_id = result["flow_id"]
|
||||||
|
|
||||||
|
mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = side_effect
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("type") == FlowResultType.FORM
|
||||||
|
assert result2.get("step_id") == "user"
|
||||||
|
assert result2.get("errors") == {"base": reason}
|
||||||
|
|
||||||
|
assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 0
|
||||||
|
|
||||||
|
mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = None
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result3.get("type") == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3.get("title") == "Test device"
|
||||||
|
assert result3.get("data") == {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
}
|
||||||
|
assert "result" in result3
|
||||||
|
assert result3["result"].unique_id == "12345"
|
||||||
|
|
||||||
|
assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 2
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate_updates_existing_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_fully_kiosk_config_flow: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test adding existing device updates existing entry."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result.get("type") == FlowResultType.FORM
|
||||||
|
assert result.get("step_id") == SOURCE_USER
|
||||||
|
assert "flow_id" in result
|
||||||
|
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2.get("type") == FlowResultType.ABORT
|
||||||
|
assert result2.get("reason") == "already_configured"
|
||||||
|
assert mock_config_entry.data == {
|
||||||
|
CONF_HOST: "1.1.1.1",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1
|
53
tests/components/fully_kiosk/test_init.py
Normal file
53
tests/components/fully_kiosk/test_init.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
"""Tests for the Fully Kiosk Browser integration."""
|
||||||
|
import asyncio
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from fullykiosk import FullyKioskError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.fully_kiosk.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_unload_config_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_fully_kiosk: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test the Fully Kiosk Browser configuration entry loading/unloading."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
assert len(mock_fully_kiosk.getDeviceInfo.mock_calls) == 1
|
||||||
|
assert len(mock_fully_kiosk.getSettings.mock_calls) == 1
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert not hass.data.get(DOMAIN)
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"side_effect",
|
||||||
|
[FullyKioskError("error", "status"), asyncio.TimeoutError],
|
||||||
|
)
|
||||||
|
async def test_config_entry_not_ready(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_fully_kiosk: MagicMock,
|
||||||
|
side_effect: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test the Fully Kiosk Browser configuration entry not ready."""
|
||||||
|
mock_fully_kiosk.getDeviceInfo.side_effect = side_effect
|
||||||
|
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
Loading…
Add table
Add a link
Reference in a new issue