Add Sanix integration (#106785)
* Add Sanix integration * Add Sanix integration * Add sanix pypi package * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Add Sanix integration * Fix ruff * Fix * Fix --------- Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
92aae4d368
commit
864c80fa55
18 changed files with 549 additions and 0 deletions
|
@ -1186,6 +1186,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/saj/ @fredericvl
|
||||
/homeassistant/components/samsungtv/ @chemelli74 @epenet
|
||||
/tests/components/samsungtv/ @chemelli74 @epenet
|
||||
/homeassistant/components/sanix/ @tomaszsluszniak
|
||||
/tests/components/sanix/ @tomaszsluszniak
|
||||
/homeassistant/components/scene/ @home-assistant/core
|
||||
/tests/components/scene/ @home-assistant/core
|
||||
/homeassistant/components/schedule/ @home-assistant/core
|
||||
|
|
37
homeassistant/components/sanix/__init__.py
Normal file
37
homeassistant/components/sanix/__init__.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""The Sanix integration."""
|
||||
|
||||
from sanix import Sanix
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
from .coordinator import SanixCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Sanix from a config entry."""
|
||||
|
||||
serial_no = entry.data[CONF_SERIAL_NUMBER]
|
||||
token = entry.data[CONF_TOKEN]
|
||||
|
||||
sanix_api = Sanix(serial_no, token)
|
||||
coordinator = SanixCoordinator(hass, sanix_api)
|
||||
|
||||
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."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
60
homeassistant/components/sanix/config_flow.py
Normal file
60
homeassistant/components/sanix/config_flow.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
"""Config flow for Sanix integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from sanix import Sanix
|
||||
from sanix.exceptions import SanixException, SanixInvalidAuthException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_SERIAL_NUMBER): str,
|
||||
vol.Required(CONF_TOKEN): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SanixConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Sanix."""
|
||||
|
||||
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:
|
||||
await self.async_set_unique_id(user_input[CONF_SERIAL_NUMBER])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
sanix_api = Sanix(user_input[CONF_SERIAL_NUMBER], user_input[CONF_TOKEN])
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(sanix_api.fetch_data)
|
||||
except SanixInvalidAuthException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SanixException:
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=MANUFACTURER,
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
description_placeholders={"dashboard_url": "https://sanix.bitcomplex.pl/"},
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
8
homeassistant/components/sanix/const.py
Normal file
8
homeassistant/components/sanix/const.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
"""Constants for the Sanix integration."""
|
||||
|
||||
CONF_SERIAL_NUMBER = "serial_number"
|
||||
|
||||
DOMAIN = "sanix"
|
||||
MANUFACTURER = "Sanix"
|
||||
|
||||
SANIX_API_HOST = "https://sanix.bitcomplex.pl"
|
36
homeassistant/components/sanix/coordinator.py
Normal file
36
homeassistant/components/sanix/coordinator.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""Sanix Coordinator."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from sanix import Sanix
|
||||
from sanix.exceptions import SanixException
|
||||
from sanix.models import Measurement
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SanixCoordinator(DataUpdateCoordinator[Measurement]):
|
||||
"""Sanix coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, sanix_api: Sanix) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=MANUFACTURER, update_interval=timedelta(hours=1)
|
||||
)
|
||||
self._sanix_api = sanix_api
|
||||
|
||||
async def _async_update_data(self) -> Measurement:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(self._sanix_api.fetch_data)
|
||||
except SanixException as err:
|
||||
raise UpdateFailed("Error while communicating with the API") from err
|
9
homeassistant/components/sanix/icons.json
Normal file
9
homeassistant/components/sanix/icons.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"fill_perc": {
|
||||
"default": "mdi:water-percent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
homeassistant/components/sanix/manifest.json
Normal file
9
homeassistant/components/sanix/manifest.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"domain": "sanix",
|
||||
"name": "Sanix",
|
||||
"codeowners": ["@tomaszsluszniak"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sanix",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["sanix==1.0.5"]
|
||||
}
|
125
homeassistant/components/sanix/sensor.py
Normal file
125
homeassistant/components/sanix/sensor.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
"""Platform for Sanix integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime
|
||||
|
||||
from sanix.const import (
|
||||
ATTR_API_BATTERY,
|
||||
ATTR_API_DEVICE_NO,
|
||||
ATTR_API_DISTANCE,
|
||||
ATTR_API_FILL_PERC,
|
||||
ATTR_API_SERVICE_DATE,
|
||||
ATTR_API_SSID,
|
||||
)
|
||||
from sanix.models import Measurement
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE, UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import SanixCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SanixSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Sanix Sensor entities."""
|
||||
|
||||
native_value_fn: Callable[[Measurement], int | datetime | date | str]
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[SanixSensorEntityDescription, ...] = (
|
||||
SanixSensorEntityDescription(
|
||||
key=ATTR_API_BATTERY,
|
||||
translation_key=ATTR_API_BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_value_fn=lambda data: data.battery,
|
||||
),
|
||||
SanixSensorEntityDescription(
|
||||
key=ATTR_API_DISTANCE,
|
||||
translation_key=ATTR_API_DISTANCE,
|
||||
native_unit_of_measurement=UnitOfLength.CENTIMETERS,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_value_fn=lambda data: data.distance,
|
||||
),
|
||||
SanixSensorEntityDescription(
|
||||
key=ATTR_API_SERVICE_DATE,
|
||||
translation_key=ATTR_API_SERVICE_DATE,
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
native_value_fn=lambda data: data.service_date,
|
||||
),
|
||||
SanixSensorEntityDescription(
|
||||
key=ATTR_API_FILL_PERC,
|
||||
translation_key=ATTR_API_FILL_PERC,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_value_fn=lambda data: data.fill_perc,
|
||||
),
|
||||
SanixSensorEntityDescription(
|
||||
key=ATTR_API_SSID,
|
||||
translation_key=ATTR_API_SSID,
|
||||
entity_registry_enabled_default=False,
|
||||
native_value_fn=lambda data: data.ssid,
|
||||
),
|
||||
SanixSensorEntityDescription(
|
||||
key=ATTR_API_DEVICE_NO,
|
||||
translation_key=ATTR_API_DEVICE_NO,
|
||||
entity_registry_enabled_default=False,
|
||||
native_value_fn=lambda data: data.device_no,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Sanix Sensor entities based on a config entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
SanixSensorEntity(coordinator, description) for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
class SanixSensorEntity(CoordinatorEntity[SanixCoordinator], SensorEntity):
|
||||
"""Sanix Sensor entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: SanixSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SanixCoordinator,
|
||||
description: SanixSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
serial_no = str(coordinator.config_entry.unique_id)
|
||||
|
||||
self._attr_unique_id = f"{serial_no}-{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_no)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=MANUFACTURER,
|
||||
serial_number=serial_no,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | datetime | date | str:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.native_value_fn(self.coordinator.data)
|
36
homeassistant/components/sanix/strings.json
Normal file
36
homeassistant/components/sanix/strings.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To get the Serial number and the Token you just have to sign in to the [Sanix Dashboard]({dashboard_url}) and open the Help -> System version page.",
|
||||
"data": {
|
||||
"serial_number": "Serial number",
|
||||
"token": "[%key:common::config_flow::data::access_token%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"service_date": {
|
||||
"name": "Service date"
|
||||
},
|
||||
"fill_perc": {
|
||||
"name": "Filled"
|
||||
},
|
||||
"device_no": {
|
||||
"name": "Device number"
|
||||
},
|
||||
"ssid": {
|
||||
"name": "SSID"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -457,6 +457,7 @@ FLOWS = {
|
|||
"rympro",
|
||||
"sabnzbd",
|
||||
"samsungtv",
|
||||
"sanix",
|
||||
"schlage",
|
||||
"scrape",
|
||||
"screenlogic",
|
||||
|
|
|
@ -5180,6 +5180,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"sanix": {
|
||||
"name": "Sanix",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"satel_integra": {
|
||||
"name": "Satel Integra",
|
||||
"integration_type": "hub",
|
||||
|
|
|
@ -2492,6 +2492,9 @@ samsungctl[websocket]==0.7.1
|
|||
# homeassistant.components.samsungtv
|
||||
samsungtvws[async,encrypted]==2.6.0
|
||||
|
||||
# homeassistant.components.sanix
|
||||
sanix==1.0.5
|
||||
|
||||
# homeassistant.components.satel_integra
|
||||
satel-integra==0.3.7
|
||||
|
||||
|
|
|
@ -1929,6 +1929,9 @@ samsungctl[websocket]==0.7.1
|
|||
# homeassistant.components.samsungtv
|
||||
samsungtvws[async,encrypted]==2.6.0
|
||||
|
||||
# homeassistant.components.sanix
|
||||
sanix==1.0.5
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.10.0
|
||||
|
||||
|
|
13
tests/components/sanix/__init__.py
Normal file
13
tests/components/sanix/__init__.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
"""Tests for Sanix."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
52
tests/components/sanix/conftest.py
Normal file
52
tests/components/sanix/conftest.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
"""Sanix tests configuration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sanix.models import Measurement
|
||||
|
||||
from homeassistant.components.sanix.const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sanix():
|
||||
"""Build a fixture for the Sanix API that connects successfully and returns measurements."""
|
||||
fixture = load_json_object_fixture("sanix/get_measurements.json")
|
||||
mock_sanix_api = MagicMock()
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.sanix.config_flow.Sanix",
|
||||
return_value=mock_sanix_api,
|
||||
) as mock_sanix_api,
|
||||
patch(
|
||||
"homeassistant.components.sanix.Sanix",
|
||||
return_value=mock_sanix_api,
|
||||
),
|
||||
):
|
||||
mock_sanix_api.return_value.fetch_data.return_value = Measurement(**fixture)
|
||||
yield mock_sanix_api
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Sanix",
|
||||
unique_id="1810088",
|
||||
data={CONF_SERIAL_NUMBER: "1234", CONF_TOKEN: "abcd"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.sanix.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
10
tests/components/sanix/fixtures/get_measurements.json
Normal file
10
tests/components/sanix/fixtures/get_measurements.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"device_no": "SANIX-1810088",
|
||||
"status": "1",
|
||||
"time": "30.12.2023 03:10:21",
|
||||
"ssid": "Wifi",
|
||||
"battery": "100",
|
||||
"distance": "109",
|
||||
"fill_perc": 32,
|
||||
"service_date": "15.06.2024"
|
||||
}
|
112
tests/components/sanix/test_config_flow.py
Normal file
112
tests/components/sanix/test_config_flow.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
"""Define tests for the Sanix config flow."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sanix.exceptions import SanixException, SanixInvalidAuthException
|
||||
|
||||
from homeassistant.components.sanix.const import (
|
||||
CONF_SERIAL_NUMBER,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
CONFIG = {CONF_SERIAL_NUMBER: "1810088", CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2"}
|
||||
|
||||
|
||||
async def test_create_entry(
|
||||
hass: HomeAssistant, mock_sanix: MagicMock, mock_setup_entry
|
||||
) -> None:
|
||||
"""Test that the user step works."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == MANUFACTURER
|
||||
assert result["data"] == {
|
||||
CONF_SERIAL_NUMBER: "1810088",
|
||||
CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2",
|
||||
}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(SanixInvalidAuthException("Invalid auth"), "invalid_auth"),
|
||||
(SanixException("Something went wrong"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_exceptions(
|
||||
hass: HomeAssistant,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
mock_sanix: MagicMock,
|
||||
mock_setup_entry,
|
||||
) -> None:
|
||||
"""Test Form exceptions."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_sanix.return_value.fetch_data.side_effect = exception
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG,
|
||||
)
|
||||
|
||||
mock_sanix.return_value.fetch_data.side_effect = None
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
CONFIG,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Sanix"
|
||||
assert result["data"] == {
|
||||
CONF_SERIAL_NUMBER: "1810088",
|
||||
CONF_TOKEN: "75868dcf8ea4c64e2063f6c4e70132d2",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_duplicate_error(
|
||||
hass: HomeAssistant, mock_sanix: MagicMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that errors are shown when duplicates are added."""
|
||||
|
||||
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"],
|
||||
CONFIG,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
27
tests/components/sanix/test_init.py
Normal file
27
tests/components/sanix/test_init.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""Test the Home Assistant analytics init module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.sanix import setup_integration
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_sanix: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test load and 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
|
Loading…
Add table
Add a link
Reference in a new issue