Add Nice G.O. integration (#122748)

* Convert Linear Garage Door to Nice G.O.

* Remove useless fixtures

* Update manifest (now cloud push! 🎉)

* Fix entry unload

* Extend config entry type

* Fix circular import

* Bump nice-go (hopefully fix dep conflict)

* Bump nice-go (moves type stubs to dev deps)

* Remove lingering mentions of Linear

* Add nice-go as logger

* Convert nice_go into a new integration and restore linear_garage_door

* Add missing new lines to snapshots

* Fixes suggested by @joostlek

* More fixes

* Fixes

* Fixes

* Fix coordinator tests

* Move coordinator tests

* Move test_no_connection_state from test_cover to test_init

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
IceBotYT 2024-08-15 12:46:06 -04:00 committed by GitHub
parent 24a20c75eb
commit 46357519e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1703 additions and 0 deletions

View File

@ -968,6 +968,8 @@ build.json @home-assistant/supervisor
/tests/components/nfandroidtv/ @tkdrob
/homeassistant/components/nibe_heatpump/ @elupus
/tests/components/nibe_heatpump/ @elupus
/homeassistant/components/nice_go/ @IceBotYT
/tests/components/nice_go/ @IceBotYT
/homeassistant/components/nightscout/ @marciogranzotto
/tests/components/nightscout/ @marciogranzotto
/homeassistant/components/nilu/ @hfurubotten

View File

@ -0,0 +1,43 @@
"""The Nice G.O. integration."""
from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import NiceGOUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.COVER]
type NiceGOConfigEntry = ConfigEntry[NiceGOUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: NiceGOConfigEntry) -> bool:
"""Set up Nice G.O. from a config entry."""
coordinator = NiceGOUpdateCoordinator(hass)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
entry.async_create_background_task(
hass,
coordinator.client_listen(),
"nice_go_websocket_task",
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: NiceGOConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
await entry.runtime_data.api.close()
return unload_ok

View File

@ -0,0 +1,68 @@
"""Config flow for Nice G.O. integration."""
from __future__ import annotations
from datetime import datetime
import logging
from typing import Any
from nice_go import AuthFailedError, NiceGOApi
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN_CREATION_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)
class NiceGOConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Nice G.O."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_EMAIL])
self._abort_if_unique_id_configured()
hub = NiceGOApi()
try:
refresh_token = await hub.authenticate(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
async_get_clientsession(self.hass),
)
except AuthFailedError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data={
CONF_EMAIL: user_input[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@ -0,0 +1,13 @@
"""Constants for the Nice G.O. integration."""
from datetime import timedelta
DOMAIN = "nice_go"
# Configuration
CONF_SITE_ID = "site_id"
CONF_DEVICE_ID = "device_id"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_REFRESH_TOKEN_CREATION_TIME = "refresh_token_creation_time"
REFRESH_TOKEN_EXPIRY_TIME = timedelta(days=30)

View File

@ -0,0 +1,220 @@
"""DataUpdateCoordinator for Nice G.O."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import datetime
import json
import logging
from typing import Any
from nice_go import (
BARRIER_STATUS,
ApiError,
AuthFailedError,
BarrierState,
ConnectionState,
NiceGOApi,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONF_REFRESH_TOKEN,
CONF_REFRESH_TOKEN_CREATION_TIME,
DOMAIN,
REFRESH_TOKEN_EXPIRY_TIME,
)
_LOGGER = logging.getLogger(__name__)
@dataclass
class NiceGODevice:
"""Nice G.O. device dataclass."""
id: str
name: str
barrier_status: str
light_status: bool
fw_version: str
connected: bool
class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
"""DataUpdateCoordinator for Nice G.O."""
config_entry: ConfigEntry
organization_id: str
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize DataUpdateCoordinator for Nice G.O."""
super().__init__(
hass,
_LOGGER,
name="Nice G.O.",
)
self.refresh_token = self.config_entry.data[CONF_REFRESH_TOKEN]
self.refresh_token_creation_time = self.config_entry.data[
CONF_REFRESH_TOKEN_CREATION_TIME
]
self.email = self.config_entry.data[CONF_EMAIL]
self.password = self.config_entry.data[CONF_PASSWORD]
self.api = NiceGOApi()
self.ws_connected = False
async def _parse_barrier(self, barrier_state: BarrierState) -> NiceGODevice | None:
"""Parse barrier data."""
device_id = barrier_state.deviceId
name = barrier_state.reported["displayName"]
if barrier_state.reported["migrationStatus"] == "NOT_STARTED":
ir.async_create_issue(
self.hass,
DOMAIN,
f"firmware_update_required_{device_id}",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="firmware_update_required",
translation_placeholders={"device_name": name},
)
return None
ir.async_delete_issue(
self.hass, DOMAIN, f"firmware_update_required_{device_id}"
)
barrier_status_raw = [
int(x) for x in barrier_state.reported["barrierStatus"].split(",")
]
if BARRIER_STATUS[int(barrier_status_raw[2])] == "STATIONARY":
barrier_status = "open" if barrier_status_raw[0] == 1 else "closed"
else:
barrier_status = BARRIER_STATUS[int(barrier_status_raw[2])].lower()
light_status = barrier_state.reported["lightStatus"].split(",")[0] == "1"
fw_version = barrier_state.reported["deviceFwVersion"]
if barrier_state.connectionState:
connected = barrier_state.connectionState.connected
else:
connected = False
return NiceGODevice(
id=device_id,
name=name,
barrier_status=barrier_status,
light_status=light_status,
fw_version=fw_version,
connected=connected,
)
async def _async_update_data(self) -> dict[str, NiceGODevice]:
return self.data
async def _async_setup(self) -> None:
"""Set up the coordinator."""
async with asyncio.timeout(10):
expiry_time = (
self.refresh_token_creation_time
+ REFRESH_TOKEN_EXPIRY_TIME.total_seconds()
)
try:
if datetime.now().timestamp() >= expiry_time:
await self._update_refresh_token()
else:
await self.api.authenticate_refresh(
self.refresh_token, async_get_clientsession(self.hass)
)
_LOGGER.debug("Authenticated with Nice G.O. API")
barriers = await self.api.get_all_barriers()
parsed_barriers = [
await self._parse_barrier(barrier.state) for barrier in barriers
]
# Parse the barriers and save them in a dictionary
devices = {
barrier.id: barrier for barrier in parsed_barriers if barrier
}
self.organization_id = await barriers[0].get_attr("organization")
except AuthFailedError as e:
raise ConfigEntryAuthFailed from e
except ApiError as e:
raise UpdateFailed from e
else:
self.async_set_updated_data(devices)
async def _update_refresh_token(self) -> None:
"""Update the refresh token with Nice G.O. API."""
_LOGGER.debug("Updating the refresh token with Nice G.O. API")
try:
refresh_token = await self.api.authenticate(
self.email, self.password, async_get_clientsession(self.hass)
)
except AuthFailedError as e:
_LOGGER.exception("Authentication failed")
raise ConfigEntryAuthFailed from e
except ApiError as e:
_LOGGER.exception("API error")
raise UpdateFailed from e
self.refresh_token = refresh_token
data = {
**self.config_entry.data,
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
}
self.hass.config_entries.async_update_entry(self.config_entry, data=data)
async def client_listen(self) -> None:
"""Listen to the websocket for updates."""
self.api.event(self.on_connected)
self.api.event(self.on_data)
try:
await self.api.connect(reconnect=True)
except ApiError:
_LOGGER.exception("API error")
if not self.hass.is_stopping:
await asyncio.sleep(5)
await self.client_listen()
async def on_data(self, data: dict[str, Any]) -> None:
"""Handle incoming data from the websocket."""
_LOGGER.debug("Received data from the websocket")
_LOGGER.debug(data)
raw_data = data["data"]["devicesStatesUpdateFeed"]["item"]
parsed_data = await self._parse_barrier(
BarrierState(
deviceId=raw_data["deviceId"],
desired=json.loads(raw_data["desired"]),
reported=json.loads(raw_data["reported"]),
connectionState=ConnectionState(
connected=raw_data["connectionState"]["connected"],
updatedTimestamp=raw_data["connectionState"]["updatedTimestamp"],
)
if raw_data["connectionState"]
else None,
version=raw_data["version"],
timestamp=raw_data["timestamp"],
)
)
if parsed_data is None:
return
data_copy = self.data.copy()
data_copy[parsed_data.id] = parsed_data
self.async_set_updated_data(data_copy)
async def on_connected(self) -> None:
"""Handle the websocket connection."""
_LOGGER.debug("Connected to the websocket")
await self.api.subscribe(self.organization_id)

View File

@ -0,0 +1,72 @@
"""Cover entity for Nice G.O."""
from typing import Any
from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import NiceGOConfigEntry
from .entity import NiceGOEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: NiceGOConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Nice G.O. cover."""
coordinator = config_entry.runtime_data
async_add_entities(
NiceGOCoverEntity(coordinator, device_id, device_data.name, "cover")
for device_id, device_data in coordinator.data.items()
)
class NiceGOCoverEntity(NiceGOEntity, CoverEntity):
"""Representation of a Nice G.O. cover."""
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
_attr_name = None
_attr_device_class = CoverDeviceClass.GARAGE
@property
def is_closed(self) -> bool:
"""Return if cover is closed."""
return self.data.barrier_status == "closed"
@property
def is_opened(self) -> bool:
"""Return if cover is open."""
return self.data.barrier_status == "open"
@property
def is_opening(self) -> bool:
"""Return if cover is opening."""
return self.data.barrier_status == "opening"
@property
def is_closing(self) -> bool:
"""Return if cover is closing."""
return self.data.barrier_status == "closing"
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the garage door."""
if self.is_closed:
return
await self.coordinator.api.close_barrier(self._device_id)
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the garage door."""
if self.is_opened:
return
await self.coordinator.api.open_barrier(self._device_id)

View File

@ -0,0 +1,41 @@
"""Base entity for Nice G.O."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import NiceGODevice, NiceGOUpdateCoordinator
class NiceGOEntity(CoordinatorEntity[NiceGOUpdateCoordinator]):
"""Common base for Nice G.O. entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: NiceGOUpdateCoordinator,
device_id: str,
device_name: str,
sub_device_id: str,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{device_id}-{sub_device_id}"
self._device_id = device_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=device_name,
sw_version=coordinator.data[device_id].fw_version,
)
@property
def data(self) -> NiceGODevice:
"""Return the Nice G.O. device."""
return self.coordinator.data[self._device_id]
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.data.connected

View File

@ -0,0 +1,10 @@
{
"domain": "nice_go",
"name": "Nice G.O.",
"codeowners": ["@IceBotYT"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nice_go",
"iot_class": "cloud_push",
"loggers": ["nice-go"],
"requirements": ["nice-go==0.1.6"]
}

View File

@ -0,0 +1,26 @@
{
"config": {
"step": {
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"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_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"issues": {
"firmware_update_required": {
"title": "Firmware update required",
"description": "Your device ({device_name}) requires a firmware update on the Nice G.O. app in order to work with this integration. Please update the firmware on the Nice G.O. app and reconfigure this integration."
}
}
}

View File

@ -385,6 +385,7 @@ FLOWS = {
"nextdns",
"nfandroidtv",
"nibe_heatpump",
"nice_go",
"nightscout",
"nina",
"nmap_tracker",

View File

@ -4029,6 +4029,12 @@
"config_flow": true,
"iot_class": "local_polling"
},
"nice_go": {
"name": "Nice G.O.",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push"
},
"nightscout": {
"name": "Nightscout",
"integration_type": "hub",

View File

@ -1419,6 +1419,9 @@ nextdns==3.1.0
# homeassistant.components.nibe_heatpump
nibe==2.11.0
# homeassistant.components.nice_go
nice-go==0.1.6
# homeassistant.components.niko_home_control
niko-home-control==0.2.1

View File

@ -1179,6 +1179,9 @@ nextdns==3.1.0
# homeassistant.components.nibe_heatpump
nibe==2.11.0
# homeassistant.components.nice_go
nice-go==0.1.6
# homeassistant.components.nfandroidtv
notifications-android-tv==0.1.5

View File

@ -0,0 +1,22 @@
"""Tests for the Nice G.O. integration."""
from unittest.mock import patch
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(
hass: HomeAssistant, config_entry: MockConfigEntry, platforms: list[Platform]
) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.nice_go.PLATFORMS",
platforms,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,78 @@
"""Common fixtures for the Nice G.O. tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from nice_go import Barrier, BarrierState, ConnectionState
import pytest
from homeassistant.components.nice_go.const import (
CONF_REFRESH_TOKEN,
CONF_REFRESH_TOKEN_CREATION_TIME,
DOMAIN,
)
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from tests.common import MockConfigEntry, load_json_array_fixture
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.nice_go.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_nice_go() -> Generator[AsyncMock]:
"""Mock a Nice G.O. client."""
with (
patch(
"homeassistant.components.nice_go.coordinator.NiceGOApi",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.nice_go.config_flow.NiceGOApi",
new=mock_client,
),
):
client = mock_client.return_value
client.authenticate.return_value = "test-refresh-token"
client.authenticate_refresh.return_value = None
client.id_token = None
client.get_all_barriers.return_value = [
Barrier(
id=barrier["id"],
type=barrier["type"],
controlLevel=barrier["controlLevel"],
attr=barrier["attr"],
state=BarrierState(
**barrier["state"],
connectionState=ConnectionState(**barrier["connectionState"]),
),
api=client,
)
for barrier in load_json_array_fixture("get_all_barriers.json", DOMAIN)
]
yield client
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
entry_id="acefdd4b3a4a0911067d1cf51414201e",
title="test-email",
data={
CONF_EMAIL: "test-email",
CONF_PASSWORD: "test-password",
CONF_REFRESH_TOKEN: "test-refresh-token",
CONF_REFRESH_TOKEN_CREATION_TIME: 1722184160.738171,
},
version=1,
unique_id="test-email",
)

View File

@ -0,0 +1,21 @@
{
"data": {
"devicesStatesUpdateFeed": {
"receiver": "ORG/0:2372",
"item": {
"deviceId": "1",
"desired": "{\"key\":\"value\"}",
"reported": "{\"displayName\":\"Test Garage 1\",\"autoDisabled\":false,\"migrationStatus\":\"DONE\",\"deviceId\":\"1\",\"lightStatus\":\"0,100\",\"vcnMode\":false,\"deviceFwVersion\":\"1.2.3.4.5.6\",\"barrierStatus\":\"0,0,1,0,-1,0,3,0\"}",
"timestamp": 123,
"version": 123,
"connectionState": {
"connected": true,
"updatedTimestamp": "123",
"__typename": "DeviceConnectionState"
},
"__typename": "DeviceState"
},
"__typename": "DeviceStateUpdateNotice"
}
}
}

View File

@ -0,0 +1,21 @@
{
"data": {
"devicesStatesUpdateFeed": {
"receiver": "ORG/0:2372",
"item": {
"deviceId": "2",
"desired": "{\"key\":\"value\"}",
"reported": "{\"displayName\":\"Test Garage 2\",\"autoDisabled\":false,\"migrationStatus\":\"DONE\",\"deviceId\":\"2\",\"lightStatus\":\"1,100\",\"vcnMode\":false,\"deviceFwVersion\":\"1.2.3.4.5.6\",\"barrierStatus\":\"1,100,2,0,-1,0,3,0\"}",
"timestamp": 123,
"version": 123,
"connectionState": {
"connected": true,
"updatedTimestamp": "123",
"__typename": "DeviceConnectionState"
},
"__typename": "DeviceState"
},
"__typename": "DeviceStateUpdateNotice"
}
}
}

View File

@ -0,0 +1,64 @@
[
{
"id": "1",
"type": "WallStation",
"controlLevel": "Owner",
"attr": [
{
"key": "organization",
"value": "test_organization"
}
],
"state": {
"deviceId": "1",
"desired": { "key": "value" },
"reported": {
"displayName": "Test Garage 1",
"autoDisabled": false,
"migrationStatus": "DONE",
"deviceId": "1",
"lightStatus": "1,100",
"vcnMode": false,
"deviceFwVersion": "1.2.3.4.5.6",
"barrierStatus": "0,0,0,0,-1,0,3,0"
},
"timestamp": null,
"version": null
},
"connectionState": {
"connected": true,
"updatedTimestamp": "123"
}
},
{
"id": "2",
"type": "WallStation",
"controlLevel": "Owner",
"attr": [
{
"key": "organization",
"value": "test_organization"
}
],
"state": {
"deviceId": "2",
"desired": { "key": "value" },
"reported": {
"displayName": "Test Garage 2",
"autoDisabled": false,
"migrationStatus": "DONE",
"deviceId": "2",
"lightStatus": "0,100",
"vcnMode": false,
"deviceFwVersion": "1.2.3.4.5.6",
"barrierStatus": "1,100,0,0,-1,0,3,0"
},
"timestamp": null,
"version": null
},
"connectionState": {
"connected": true,
"updatedTimestamp": "123"
}
}
]

View File

@ -0,0 +1,193 @@
# serializer version: 1
# name: test_covers[cover.test_garage_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.test_garage_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'nice_go',
'previous_unique_id': None,
'supported_features': <CoverEntityFeature: 3>,
'translation_key': None,
'unique_id': '1-cover',
'unit_of_measurement': None,
})
# ---
# name: test_covers[cover.test_garage_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Test Garage 1',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_garage_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---
# name: test_covers[cover.test_garage_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.test_garage_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'nice_go',
'previous_unique_id': None,
'supported_features': <CoverEntityFeature: 3>,
'translation_key': None,
'unique_id': '2-cover',
'unit_of_measurement': None,
})
# ---
# name: test_covers[cover.test_garage_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Test Garage 2',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_garage_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'open',
})
# ---
# name: test_covers[cover.test_garage_3-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.test_garage_3',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'linear_garage_door',
'previous_unique_id': None,
'supported_features': <CoverEntityFeature: 3>,
'translation_key': None,
'unique_id': 'test3-GDO',
'unit_of_measurement': None,
})
# ---
# name: test_covers[cover.test_garage_3-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Test Garage 3',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_garage_3',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'opening',
})
# ---
# name: test_covers[cover.test_garage_4-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'cover',
'entity_category': None,
'entity_id': 'cover.test_garage_4',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <CoverDeviceClass.GARAGE: 'garage'>,
'original_icon': None,
'original_name': None,
'platform': 'linear_garage_door',
'previous_unique_id': None,
'supported_features': <CoverEntityFeature: 3>,
'translation_key': None,
'unique_id': 'test4-GDO',
'unit_of_measurement': None,
})
# ---
# name: test_covers[cover.test_garage_4-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Test Garage 4',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_garage_4',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closing',
})
# ---

View File

@ -0,0 +1,43 @@
# serializer version: 1
# name: test_entry_diagnostics
dict({
'coordinator_data': dict({
'1': dict({
'barrier_status': 'closed',
'connected': True,
'fw_version': '1.2.3.4.5.6',
'id': '1',
'light_status': True,
'name': 'Test Garage 1',
}),
'2': dict({
'barrier_status': 'open',
'connected': True,
'fw_version': '1.2.3.4.5.6',
'id': '2',
'light_status': False,
'name': 'Test Garage 2',
}),
}),
'entry': dict({
'data': dict({
'email': '**REDACTED**',
'password': '**REDACTED**',
'refresh_token': '**REDACTED**',
'refresh_token_creation_time': 1722184160.738171,
}),
'disabled_by': None,
'domain': 'nice_go',
'entry_id': 'acefdd4b3a4a0911067d1cf51414201e',
'minor_version': 1,
'options': dict({
}),
'pref_disable_new_entities': False,
'pref_disable_polling': False,
'source': 'user',
'title': '**REDACTED**',
'unique_id': None,
'version': 1,
}),
})
# ---

View File

@ -0,0 +1,16 @@
# serializer version: 1
# name: test_on_data_none_parsed
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'garage',
'friendly_name': 'Test Garage 1',
'supported_features': <CoverEntityFeature: 3>,
}),
'context': <ANY>,
'entity_id': 'cover.test_garage_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'closed',
})
# ---

View File

@ -0,0 +1,223 @@
# serializer version: 1
# name: test_data[light.test_garage_1_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.test_garage_1_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'nice_go',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '1-light',
'unit_of_measurement': None,
})
# ---
# name: test_data[light.test_garage_1_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': <ColorMode.ONOFF: 'onoff'>,
'friendly_name': 'Test Garage 1 Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.test_garage_1_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_data[light.test_garage_2_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.test_garage_2_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'nice_go',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': '2-light',
'unit_of_measurement': None,
})
# ---
# name: test_data[light.test_garage_2_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'color_mode': None,
'friendly_name': 'Test Garage 2 Light',
'supported_color_modes': list([
<ColorMode.ONOFF: 'onoff'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.test_garage_2_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_data[light.test_garage_3_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.test_garage_3_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'linear_garage_door',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': 'test3-Light',
'unit_of_measurement': None,
})
# ---
# name: test_data[light.test_garage_3_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': None,
'color_mode': None,
'friendly_name': 'Test Garage 3 Light',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.test_garage_3_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_data[light.test_garage_4_light-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'light',
'entity_category': None,
'entity_id': 'light.test_garage_4_light',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Light',
'platform': 'linear_garage_door',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'light',
'unique_id': 'test4-Light',
'unit_of_measurement': None,
})
# ---
# name: test_data[light.test_garage_4_light-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'brightness': 255,
'color_mode': <ColorMode.BRIGHTNESS: 'brightness'>,
'friendly_name': 'Test Garage 4 Light',
'supported_color_modes': list([
<ColorMode.BRIGHTNESS: 'brightness'>,
]),
'supported_features': <LightEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'light.test_garage_4_light',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -0,0 +1,111 @@
"""Test the Nice G.O. config flow."""
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from nice_go import AuthFailedError
import pytest
from homeassistant.components.nice_go.const import (
CONF_REFRESH_TOKEN,
CONF_REFRESH_TOKEN_CREATION_TIME,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
async def test_form(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_setup_entry: AsyncMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_EMAIL: "test-email",
CONF_PASSWORD: "test-password",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "test-email"
assert result["data"][CONF_EMAIL] == "test-email"
assert result["data"][CONF_PASSWORD] == "test-password"
assert result["data"][CONF_REFRESH_TOKEN] == "test-refresh-token"
assert CONF_REFRESH_TOKEN_CREATION_TIME in result["data"]
assert result["result"].unique_id == "test-email"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("side_effect", "expected_error"),
[(AuthFailedError, "invalid_auth"), (Exception, "unknown")],
)
async def test_form_exceptions(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_setup_entry: AsyncMock,
side_effect: Exception,
expected_error: str,
) -> None:
"""Test we handle invalid auth."""
mock_nice_go.authenticate.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_EMAIL: "test-email",
CONF_PASSWORD: "test-password",
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": expected_error}
mock_nice_go.authenticate.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_EMAIL: "test-email",
CONF_PASSWORD: "test-password",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_duplicate_device(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nice_go: AsyncMock,
) -> None:
"""Test that duplicate devices are handled."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_EMAIL: "test-email",
CONF_PASSWORD: "test-password",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,115 @@
"""Test Nice G.O. cover."""
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from syrupy import SnapshotAssertion
from homeassistant.components.cover import (
DOMAIN as COVER_DOMAIN,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
)
from homeassistant.components.nice_go.const import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform
async def test_covers(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that data gets parsed and returned appropriately."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_open_cover(
hass: HomeAssistant, mock_nice_go: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test that opening the cover works as intended."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.test_garage_2"},
blocking=True,
)
assert mock_nice_go.open_barrier.call_count == 0
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.test_garage_1"},
blocking=True,
)
assert mock_nice_go.open_barrier.call_count == 1
async def test_close_cover(
hass: HomeAssistant, mock_nice_go: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test that closing the cover works as intended."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.test_garage_1"},
blocking=True,
)
assert mock_nice_go.close_barrier.call_count == 0
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.test_garage_2"},
blocking=True,
)
assert mock_nice_go.close_barrier.call_count == 1
async def test_update_cover_state(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that closing the cover works as intended."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert hass.states.get("cover.test_garage_1").state == STATE_CLOSED
assert hass.states.get("cover.test_garage_2").state == STATE_OPEN
device_update = load_json_object_fixture("device_state_update.json", DOMAIN)
await mock_config_entry.runtime_data.on_data(device_update)
device_update_1 = load_json_object_fixture("device_state_update_1.json", DOMAIN)
await mock_config_entry.runtime_data.on_data(device_update_1)
assert hass.states.get("cover.test_garage_1").state == STATE_OPENING
assert hass.states.get("cover.test_garage_2").state == STATE_CLOSING

View File

@ -0,0 +1,288 @@
"""Test Nice G.O. init."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
from nice_go import ApiError, AuthFailedError, Barrier, BarrierState
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.nice_go.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_unload_entry(
hass: HomeAssistant, mock_nice_go: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test the unload entry."""
await setup_integration(hass, mock_config_entry, [])
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
("side_effect", "entry_state"),
[
(
AuthFailedError(),
ConfigEntryState.SETUP_ERROR,
),
(ApiError(), ConfigEntryState.SETUP_RETRY),
],
)
async def test_setup_failure(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
side_effect: Exception,
entry_state: ConfigEntryState,
) -> None:
"""Test reauth trigger setup."""
mock_nice_go.authenticate_refresh.side_effect = side_effect
await setup_integration(hass, mock_config_entry, [])
assert mock_config_entry.state is entry_state
async def test_firmware_update_required(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test firmware update required."""
mock_nice_go.get_all_barriers.return_value = [
Barrier(
id="test-device-id",
type="test-type",
controlLevel="test-control-level",
attr=[{"key": "test-attr", "value": "test-value"}],
state=BarrierState(
deviceId="test-device-id",
reported={
"displayName": "test-display-name",
"migrationStatus": "NOT_STARTED",
},
desired=None,
connectionState=None,
version=None,
timestamp=None,
),
api=mock_nice_go,
)
]
await setup_integration(hass, mock_config_entry, [])
issue = issue_registry.async_get_issue(
DOMAIN,
"firmware_update_required_test-device-id",
)
assert issue
async def test_update_refresh_token(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test updating refresh token."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert mock_nice_go.authenticate_refresh.call_count == 1
assert mock_nice_go.get_all_barriers.call_count == 1
assert mock_nice_go.authenticate.call_count == 0
mock_nice_go.authenticate.return_value = "new-refresh-token"
freezer.tick(timedelta(days=30))
async_fire_time_changed(hass)
assert await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_nice_go.authenticate_refresh.call_count == 1
assert mock_nice_go.authenticate.call_count == 1
assert mock_nice_go.get_all_barriers.call_count == 2
assert mock_config_entry.data["refresh_token"] == "new-refresh-token"
async def test_update_refresh_token_api_error(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test updating refresh token with error."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert mock_nice_go.authenticate_refresh.call_count == 1
assert mock_nice_go.get_all_barriers.call_count == 1
assert mock_nice_go.authenticate.call_count == 0
mock_nice_go.authenticate.side_effect = ApiError
freezer.tick(timedelta(days=30))
async_fire_time_changed(hass)
assert not await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_nice_go.authenticate_refresh.call_count == 1
assert mock_nice_go.authenticate.call_count == 1
assert mock_nice_go.get_all_barriers.call_count == 1
assert mock_config_entry.data["refresh_token"] == "test-refresh-token"
assert "API error" in caplog.text
async def test_update_refresh_token_auth_failed(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test updating refresh token with error."""
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert mock_nice_go.authenticate_refresh.call_count == 1
assert mock_nice_go.get_all_barriers.call_count == 1
assert mock_nice_go.authenticate.call_count == 0
mock_nice_go.authenticate.side_effect = AuthFailedError
freezer.tick(timedelta(days=30))
async_fire_time_changed(hass)
assert not await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_nice_go.authenticate_refresh.call_count == 1
assert mock_nice_go.authenticate.call_count == 1
assert mock_nice_go.get_all_barriers.call_count == 1
assert mock_config_entry.data["refresh_token"] == "test-refresh-token"
assert "Authentication failed" in caplog.text
async def test_client_listen_api_error(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test client listen with error."""
mock_nice_go.connect.side_effect = ApiError
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert "API error" in caplog.text
mock_nice_go.connect.side_effect = None
freezer.tick(timedelta(seconds=5))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_nice_go.connect.call_count == 2
async def test_on_data_none_parsed(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test on data with None parsed."""
mock_nice_go.event = MagicMock()
await setup_integration(hass, mock_config_entry, [Platform.COVER])
await mock_nice_go.event.call_args[0][0](
{
"data": {
"devicesStatesUpdateFeed": {
"item": {
"deviceId": "1",
"desired": '{"key": "value"}',
"reported": '{"displayName":"test-display-name", "migrationStatus":"NOT_STARTED"}',
"connectionState": {
"connected": None,
"updatedTimestamp": None,
},
"version": None,
"timestamp": None,
}
}
}
}
)
assert hass.states.get("cover.test_garage_1") == snapshot
async def test_on_connected(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test on connected."""
mock_nice_go.event = MagicMock()
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert mock_nice_go.event.call_count == 2
mock_nice_go.subscribe = AsyncMock()
await mock_nice_go.event.call_args_list[0][0][0]()
assert mock_nice_go.subscribe.call_count == 1
async def test_no_connection_state(
hass: HomeAssistant,
mock_nice_go: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test parsing barrier with no connection state."""
mock_nice_go.event = MagicMock()
await setup_integration(hass, mock_config_entry, [Platform.COVER])
assert mock_nice_go.event.call_count == 2
await mock_nice_go.event.call_args[0][0](
{
"data": {
"devicesStatesUpdateFeed": {
"item": {
"deviceId": "1",
"desired": '{"key": "value"}',
"reported": '{"displayName":"Test Garage 1", "migrationStatus":"DONE", "barrierStatus": "1,100,0", "deviceFwVersion": "1.0.0", "lightStatus": "1,100"}',
"connectionState": None,
"version": None,
"timestamp": None,
}
}
}
}
)
assert hass.states.get("cover.test_garage_1").state == "unavailable"