Bump Intellifire to 4.1.9 (#121091)

* rebase

* Minor patch to fix duplicate DeviceInfo beign created - if data hasnt updated yet

* rebase

* Minor patch to fix duplicate DeviceInfo beign created - if data hasnt updated yet

* fixing formatting

* Update homeassistant/components/intellifire/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/intellifire/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Removing cloud connectivity sensor - leaving local one in

* Renaming class to something more useful

* addressing pr

* Update homeassistant/components/intellifire/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* add ruff exception

* Fix test annotations

* remove access to private variable

* Bumping to 4.1.9 instead of 4.1.5

* A renaming

* rename

* Updated testing

* Update __init__.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* updateing styrings

* Update tests/components/intellifire/conftest.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Testing refactor - WIP

* everything is passing - cleanup still needed

* cleaning up comments

* update pr

* unrename

* Update homeassistant/components/intellifire/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* fixing sentence

* fixed fixture and removed error codes

* reverted a bad change

* fixing strings.json

* revert renaming

* fix

* typing inother pr

* adding extra tests - one has a really dumb name

* using a real value

* added a migration in

* Update homeassistant/components/intellifire/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update tests/components/intellifire/test_init.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* cleanup continues

* addressing pr

* switch back to debug

* Update tests/components/intellifire/conftest.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* some changes

* restore property mock cuase didnt work otherwise

* cleanup has begun

* removed extra text

* addressing pr stuff

* fixed reauth

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
pull/124880/head
Jeef 2024-09-01 04:48:38 -06:00 committed by Bram Kragten
parent 411b014da2
commit 234f32265e
28 changed files with 2445 additions and 678 deletions

View File

@ -2,15 +2,17 @@
from __future__ import annotations from __future__ import annotations
from aiohttp import ClientConnectionError import asyncio
from intellifire4py import IntellifireControlAsync
from intellifire4py.exceptions import LoginException from intellifire4py import UnifiedFireplace
from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal from intellifire4py.cloud_interface import IntelliFireCloudInterface
from intellifire4py.model import IntelliFireCommonFireplaceData
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_API_KEY,
CONF_HOST, CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD, CONF_PASSWORD,
CONF_USERNAME, CONF_USERNAME,
Platform, Platform,
@ -18,7 +20,18 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import CONF_USER_ID, DOMAIN, LOGGER from .const import (
CONF_AUTH_COOKIE,
CONF_CONTROL_MODE,
CONF_READ_MODE,
CONF_SERIAL,
CONF_USER_ID,
CONF_WEB_CLIENT_ID,
DOMAIN,
INIT_WAIT_TIME_SECONDS,
LOGGER,
STARTUP_TIMEOUT,
)
from .coordinator import IntellifireDataUpdateCoordinator from .coordinator import IntellifireDataUpdateCoordinator
PLATFORMS = [ PLATFORMS = [
@ -32,79 +45,114 @@ PLATFORMS = [
] ]
def _construct_common_data(entry: ConfigEntry) -> IntelliFireCommonFireplaceData:
"""Convert config entry data into IntelliFireCommonFireplaceData."""
return IntelliFireCommonFireplaceData(
auth_cookie=entry.data[CONF_AUTH_COOKIE],
user_id=entry.data[CONF_USER_ID],
web_client_id=entry.data[CONF_WEB_CLIENT_ID],
serial=entry.data[CONF_SERIAL],
api_key=entry.data[CONF_API_KEY],
ip_address=entry.data[CONF_IP_ADDRESS],
read_mode=entry.options[CONF_READ_MODE],
control_mode=entry.options[CONF_CONTROL_MODE],
)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate entries."""
LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version == 1:
new = {**config_entry.data}
if config_entry.minor_version < 2:
username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD]
# Create a Cloud Interface
async with IntelliFireCloudInterface() as cloud_interface:
await cloud_interface.login_with_credentials(
username=username, password=password
)
new_data = cloud_interface.user_data.get_data_for_ip(new[CONF_HOST])
if not new_data:
raise ConfigEntryAuthFailed
new[CONF_API_KEY] = new_data.api_key
new[CONF_WEB_CLIENT_ID] = new_data.web_client_id
new[CONF_AUTH_COOKIE] = new_data.auth_cookie
new[CONF_IP_ADDRESS] = new_data.ip_address
new[CONF_SERIAL] = new_data.serial
hass.config_entries.async_update_entry(
config_entry,
data=new,
options={CONF_READ_MODE: "local", CONF_CONTROL_MODE: "local"},
unique_id=new[CONF_SERIAL],
version=1,
minor_version=2,
)
LOGGER.debug("Pseudo Migration %s successful", config_entry.version)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IntelliFire from a config entry.""" """Set up IntelliFire from a config entry."""
LOGGER.debug("Setting up config entry: %s", entry.unique_id)
if CONF_USERNAME not in entry.data: if CONF_USERNAME not in entry.data:
LOGGER.debug("Old config entry format detected: %s", entry.unique_id) LOGGER.debug("Config entry without username detected: %s", entry.unique_id)
raise ConfigEntryAuthFailed raise ConfigEntryAuthFailed
ift_control = IntellifireControlAsync(
fireplace_ip=entry.data[CONF_HOST],
)
try: try:
await ift_control.login( fireplace: UnifiedFireplace = (
username=entry.data[CONF_USERNAME], await UnifiedFireplace.build_fireplace_from_common(
password=entry.data[CONF_PASSWORD], _construct_common_data(entry)
)
) )
except (ConnectionError, ClientConnectionError) as err: LOGGER.debug("Waiting for Fireplace to Initialize")
raise ConfigEntryNotReady from err await asyncio.wait_for(
except LoginException as err: _async_wait_for_initialization(fireplace), timeout=STARTUP_TIMEOUT
raise ConfigEntryAuthFailed(err) from err
finally:
await ift_control.close()
# Extract API Key and User_ID from ift_control
# Eventually this will migrate to using IntellifireAPICloud
if CONF_USER_ID not in entry.data or CONF_API_KEY not in entry.data:
LOGGER.info(
"Updating intellifire config entry for %s with api information",
entry.unique_id,
)
cloud_api = IntellifireAPICloud()
await cloud_api.login(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
api_key = cloud_api.get_fireplace_api_key()
user_id = cloud_api.get_user_id()
# Update data entry
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_API_KEY: api_key,
CONF_USER_ID: user_id,
},
) )
except TimeoutError as err:
raise ConfigEntryNotReady(
"Initialization of fireplace timed out after 10 minutes"
) from err
else: # Construct coordinator
api_key = entry.data[CONF_API_KEY] data_update_coordinator = IntellifireDataUpdateCoordinator(
user_id = entry.data[CONF_USER_ID] hass=hass, fireplace=fireplace
# Instantiate local control
api = IntellifireAPILocal(
fireplace_ip=entry.data[CONF_HOST],
api_key=api_key,
user_id=user_id,
) )
# Define the update coordinator LOGGER.debug("Fireplace to Initialized - Awaiting first refresh")
coordinator = IntellifireDataUpdateCoordinator( await data_update_coordinator.async_config_entry_first_refresh()
hass=hass,
api=api, hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_update_coordinator
)
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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def _async_wait_for_initialization(
fireplace: UnifiedFireplace, timeout=STARTUP_TIMEOUT
):
"""Wait for a fireplace to be initialized."""
while (
fireplace.data.ipv4_address == "127.0.0.1" and fireplace.data.serial == "unset"
):
LOGGER.debug(f"Waiting for fireplace to initialize [{fireplace.read_mode}]")
await asyncio.sleep(INIT_WAIT_TIME_SECONDS)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from intellifire4py import IntellifirePollData from intellifire4py.model import IntelliFirePollData
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -26,7 +26,7 @@ from .entity import IntellifireEntity
class IntellifireBinarySensorRequiredKeysMixin: class IntellifireBinarySensorRequiredKeysMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
value_fn: Callable[[IntellifirePollData], bool] value_fn: Callable[[IntelliFirePollData], bool]
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@ -69,7 +69,7 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity):
super().__init__(coordinator, description) super().__init__(coordinator, description)
if coordinator.data.thermostat_on: if coordinator.data.thermostat_on:
self.last_temp = coordinator.data.thermostat_setpoint_c self.last_temp = int(coordinator.data.thermostat_setpoint_c)
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:

View File

@ -7,16 +7,33 @@ from dataclasses import dataclass
from typing import Any from typing import Any
from aiohttp import ClientConnectionError from aiohttp import ClientConnectionError
from intellifire4py import AsyncUDPFireplaceFinder from intellifire4py.cloud_interface import IntelliFireCloudInterface
from intellifire4py.exceptions import LoginException from intellifire4py.exceptions import LoginError
from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal from intellifire4py.local_api import IntelliFireAPILocal
from intellifire4py.model import IntelliFireCommonFireplaceData
import voluptuous as vol import voluptuous as vol
from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_USERNAME,
)
from .const import CONF_USER_ID, DOMAIN, LOGGER from .const import (
API_MODE_LOCAL,
CONF_AUTH_COOKIE,
CONF_CONTROL_MODE,
CONF_READ_MODE,
CONF_SERIAL,
CONF_USER_ID,
CONF_WEB_CLIENT_ID,
DOMAIN,
LOGGER,
)
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
@ -31,17 +48,20 @@ class DiscoveredHostInfo:
serial: str | None serial: str | None
async def validate_host_input(host: str, dhcp_mode: bool = False) -> str: async def _async_poll_local_fireplace_for_serial(
host: str, dhcp_mode: bool = False
) -> str:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
""" """
LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host) LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host)
api = IntellifireAPILocal(fireplace_ip=host) api = IntelliFireAPILocal(fireplace_ip=host)
await api.poll(suppress_warnings=dhcp_mode) await api.poll(suppress_warnings=dhcp_mode)
serial = api.data.serial serial = api.data.serial
LOGGER.debug("Found a fireplace: %s", serial) LOGGER.debug("Found a fireplace: %s", serial)
# Return the serial number which will be used to calculate a unique ID for the device/sensors # Return the serial number which will be used to calculate a unique ID for the device/sensors
return serial return serial
@ -50,239 +70,206 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for IntelliFire.""" """Handle a config flow for IntelliFire."""
VERSION = 1 VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the Config Flow Handler.""" """Initialize the Config Flow Handler."""
self._host: str = ""
self._serial: str = "" # DHCP Variables
self._not_configured_hosts: list[DiscoveredHostInfo] = [] self._dhcp_discovered_serial: str = "" # used only in discovery mode
self._discovered_host: DiscoveredHostInfo self._discovered_host: DiscoveredHostInfo
self._dhcp_mode = False
self._is_reauth = False
self._not_configured_hosts: list[DiscoveredHostInfo] = []
self._reauth_needed: DiscoveredHostInfo self._reauth_needed: DiscoveredHostInfo
async def _find_fireplaces(self): self._configured_serials: list[str] = []
"""Perform UDP discovery."""
fireplace_finder = AsyncUDPFireplaceFinder()
discovered_hosts = await fireplace_finder.search_fireplace(timeout=12)
configured_hosts = {
entry.data[CONF_HOST]
for entry in self._async_current_entries(include_ignore=False)
if CONF_HOST in entry.data # CONF_HOST will be missing for ignored entries
}
self._not_configured_hosts = [ # Define a cloud api interface we can use
DiscoveredHostInfo(ip, None) self.cloud_api_interface = IntelliFireCloudInterface()
for ip in discovered_hosts
if ip not in configured_hosts
]
LOGGER.debug("Discovered Hosts: %s", discovered_hosts)
LOGGER.debug("Configured Hosts: %s", configured_hosts)
LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts)
async def validate_api_access_and_create_or_update(
self, *, host: str, username: str, password: str, serial: str
):
"""Validate username/password against api."""
LOGGER.debug("Attempting login to iftapi with: %s", username)
ift_cloud = IntellifireAPICloud()
await ift_cloud.login(username=username, password=password)
api_key = ift_cloud.get_fireplace_api_key()
user_id = ift_cloud.get_user_id()
data = {
CONF_HOST: host,
CONF_PASSWORD: password,
CONF_USERNAME: username,
CONF_API_KEY: api_key,
CONF_USER_ID: user_id,
}
# Update or Create
existing_entry = await self.async_set_unique_id(serial)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=data)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=f"Fireplace {serial}", data=data)
async def async_step_api_config(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Configure API access."""
errors = {}
control_schema = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
if user_input is not None:
control_schema = vol.Schema(
{
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
): str,
vol.Required(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
}
)
try:
return await self.validate_api_access_and_create_or_update(
host=self._host,
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
serial=self._serial,
)
except (ConnectionError, ClientConnectionError):
errors["base"] = "iftapi_connect"
LOGGER.error(
"Could not connect to iftapi.net over https - verify connectivity"
)
except LoginException:
errors["base"] = "api_error"
LOGGER.error("Invalid credentials for iftapi.net")
return self.async_show_form(
step_id="api_config", errors=errors, data_schema=control_schema
)
async def _async_validate_ip_and_continue(self, host: str) -> ConfigFlowResult:
"""Validate local config and continue."""
self._async_abort_entries_match({CONF_HOST: host})
self._serial = await validate_host_input(host)
await self.async_set_unique_id(self._serial, raise_on_progress=False)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# Store current data and jump to next stage
self._host = host
return await self.async_step_api_config()
async def async_step_manual_device_entry(self, user_input=None):
"""Handle manual input of local IP configuration."""
LOGGER.debug("STEP: manual_device_entry")
errors = {}
self._host = user_input.get(CONF_HOST) if user_input else None
if user_input is not None:
try:
return await self._async_validate_ip_and_continue(self._host)
except (ConnectionError, ClientConnectionError):
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="manual_device_entry",
errors=errors,
data_schema=vol.Schema({vol.Required(CONF_HOST, default=self._host): str}),
)
async def async_step_pick_device(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick which device to configure."""
errors = {}
LOGGER.debug("STEP: pick_device")
if user_input is not None:
if user_input[CONF_HOST] == MANUAL_ENTRY_STRING:
return await self.async_step_manual_device_entry()
try:
return await self._async_validate_ip_and_continue(user_input[CONF_HOST])
except (ConnectionError, ClientConnectionError):
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="pick_device",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): vol.In(
[host.ip for host in self._not_configured_hosts]
+ [MANUAL_ENTRY_STRING]
)
}
),
)
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Start the user flow.""" """Start the user flow."""
# Launch fireplaces discovery current_entries = self._async_current_entries(include_ignore=False)
await self._find_fireplaces() self._configured_serials = [
LOGGER.debug("STEP: user") entry.data[CONF_SERIAL] for entry in current_entries
if self._not_configured_hosts: ]
LOGGER.debug("Running Step: pick_device")
return await self.async_step_pick_device() return await self.async_step_cloud_api()
LOGGER.debug("Running Step: manual_device_entry")
return await self.async_step_manual_device_entry() async def async_step_cloud_api(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Authenticate against IFTAPI Cloud in order to see configured devices.
Local control of IntelliFire devices requires that the user download the correct API KEY which is only available on the cloud. Cloud control of the devices requires the user has at least once authenticated against the cloud and a set of cookie variables have been stored locally.
"""
errors: dict[str, str] = {}
LOGGER.debug("STEP: cloud_api")
if user_input is not None:
try:
async with self.cloud_api_interface as cloud_interface:
await cloud_interface.login_with_credentials(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
# If login was successful pass username/password to next step
return await self.async_step_pick_cloud_device()
except LoginError:
errors["base"] = "api_error"
return self.async_show_form(
step_id="cloud_api",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
),
)
async def async_step_pick_cloud_device(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step to select a device from the cloud.
We can only get here if we have logged in. If there is only one device available it will be auto-configured,
else the user will be given a choice to pick a device.
"""
errors: dict[str, str] = {}
LOGGER.debug(
f"STEP: pick_cloud_device: {user_input} - DHCP_MODE[{self._dhcp_mode}"
)
if self._dhcp_mode or user_input is not None:
if self._dhcp_mode:
serial = self._dhcp_discovered_serial
LOGGER.debug(f"DHCP Mode detected for serial [{serial}]")
if user_input is not None:
serial = user_input[CONF_SERIAL]
# Run a unique ID Check prior to anything else
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured(updates={CONF_SERIAL: serial})
# If Serial is Good obtain fireplace and configure
fireplace = self.cloud_api_interface.user_data.get_data_for_serial(serial)
if fireplace:
return await self._async_create_config_entry_from_common_data(
fireplace=fireplace
)
# Parse User Data to see if we auto-configure or prompt for selection:
user_data = self.cloud_api_interface.user_data
available_fireplaces: list[IntelliFireCommonFireplaceData] = [
fp
for fp in user_data.fireplaces
if fp.serial not in self._configured_serials
]
# Abort if all devices have been configured
if not available_fireplaces:
return self.async_abort(reason="no_available_devices")
# If there is a single fireplace configure it
if len(available_fireplaces) == 1:
if self._is_reauth:
reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self._async_create_config_entry_from_common_data(
fireplace=available_fireplaces[0], existing_entry=reauth_entry
)
return await self._async_create_config_entry_from_common_data(
fireplace=available_fireplaces[0]
)
return self.async_show_form(
step_id="pick_cloud_device",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_SERIAL): vol.In(
[fp.serial for fp in available_fireplaces]
)
}
),
)
async def _async_create_config_entry_from_common_data(
self,
fireplace: IntelliFireCommonFireplaceData,
existing_entry: ConfigEntry | None = None,
) -> ConfigFlowResult:
"""Construct a config entry based on an object of IntelliFireCommonFireplaceData."""
data = {
CONF_IP_ADDRESS: fireplace.ip_address,
CONF_API_KEY: fireplace.api_key,
CONF_SERIAL: fireplace.serial,
CONF_AUTH_COOKIE: fireplace.auth_cookie,
CONF_WEB_CLIENT_ID: fireplace.web_client_id,
CONF_USER_ID: fireplace.user_id,
CONF_USERNAME: self.cloud_api_interface.user_data.username,
CONF_PASSWORD: self.cloud_api_interface.user_data.password,
}
options = {CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_LOCAL}
if existing_entry:
return self.async_update_reload_and_abort(
existing_entry, data=data, options=options
)
return self.async_create_entry(
title=f"Fireplace {fireplace.serial}", data=data, options=options
)
async def async_step_reauth( async def async_step_reauth(
self, entry_data: Mapping[str, Any] self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error.""" """Perform reauth upon an API authentication error."""
LOGGER.debug("STEP: reauth") LOGGER.debug("STEP: reauth")
self._is_reauth = True
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry
assert entry.unique_id
# populate the expected vars # populate the expected vars
self._serial = entry.unique_id self._dhcp_discovered_serial = entry.data[CONF_SERIAL] # type: ignore[union-attr]
self._host = entry.data[CONF_HOST]
placeholders = {CONF_HOST: self._host, "serial": self._serial} placeholders = {"serial": self._dhcp_discovered_serial}
self.context["title_placeholders"] = placeholders self.context["title_placeholders"] = placeholders
return await self.async_step_api_config()
return await self.async_step_cloud_api()
async def async_step_dhcp( async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle DHCP Discovery.""" """Handle DHCP Discovery."""
self._dhcp_mode = True
# Run validation logic on ip # Run validation logic on ip
host = discovery_info.ip ip_address = discovery_info.ip
LOGGER.debug("STEP: dhcp for host %s", host) LOGGER.debug("STEP: dhcp for ip_address %s", ip_address)
self._async_abort_entries_match({CONF_HOST: host}) self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
try: try:
self._serial = await validate_host_input(host, dhcp_mode=True) self._dhcp_discovered_serial = await _async_poll_local_fireplace_for_serial(
ip_address, dhcp_mode=True
)
except (ConnectionError, ClientConnectionError): except (ConnectionError, ClientConnectionError):
LOGGER.debug( LOGGER.debug(
"DHCP Discovery has determined %s is not an IntelliFire device", host "DHCP Discovery has determined %s is not an IntelliFire device",
ip_address,
) )
return self.async_abort(reason="not_intellifire_device") return self.async_abort(reason="not_intellifire_device")
await self.async_set_unique_id(self._serial) return await self.async_step_cloud_api()
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._discovered_host = DiscoveredHostInfo(ip=host, serial=self._serial)
placeholders = {CONF_HOST: host, "serial": self._serial}
self.context["title_placeholders"] = placeholders
self._set_confirm_only()
return await self.async_step_dhcp_confirm()
async def async_step_dhcp_confirm(self, user_input=None):
"""Attempt to confirm."""
LOGGER.debug("STEP: dhcp_confirm")
# Add the hosts one by one
host = self._discovered_host.ip
serial = self._discovered_host.serial
if user_input is None:
# Show the confirmation dialog
return self.async_show_form(
step_id="dhcp_confirm",
description_placeholders={CONF_HOST: host, "serial": serial},
)
return self.async_create_entry(
title=f"Fireplace {serial}",
data={CONF_HOST: host},
)

View File

@ -5,11 +5,22 @@ from __future__ import annotations
import logging import logging
DOMAIN = "intellifire" DOMAIN = "intellifire"
CONF_USER_ID = "user_id"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
DEFAULT_THERMOSTAT_TEMP = 21
CONF_USER_ID = "user_id" # part of the cloud cookie
CONF_WEB_CLIENT_ID = "web_client_id" # part of the cloud cookie
CONF_AUTH_COOKIE = "auth_cookie" # part of the cloud cookie
CONF_SERIAL = "serial" CONF_SERIAL = "serial"
CONF_READ_MODE = "cloud_read"
CONF_CONTROL_MODE = "cloud_control"
DEFAULT_THERMOSTAT_TEMP = 21
API_MODE_LOCAL = "local"
API_MODE_CLOUD = "cloud"
STARTUP_TIMEOUT = 600
INIT_WAIT_TIME_SECONDS = 10

View File

@ -2,27 +2,27 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from datetime import timedelta from datetime import timedelta
from aiohttp import ClientConnectionError from intellifire4py import UnifiedFireplace
from intellifire4py import IntellifirePollData from intellifire4py.control import IntelliFireController
from intellifire4py.intellifire import IntellifireAPILocal from intellifire4py.model import IntelliFirePollData
from intellifire4py.read import IntelliFireDataProvider
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER from .const import DOMAIN, LOGGER
class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData]): class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData]):
"""Class to manage the polling of the fireplace API.""" """Class to manage the polling of the fireplace API."""
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
api: IntellifireAPILocal, fireplace: UnifiedFireplace,
) -> None: ) -> None:
"""Initialize the Coordinator.""" """Initialize the Coordinator."""
super().__init__( super().__init__(
@ -31,36 +31,21 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
name=DOMAIN, name=DOMAIN,
update_interval=timedelta(seconds=15), update_interval=timedelta(seconds=15),
) )
self._api = api
async def _async_update_data(self) -> IntellifirePollData: self.fireplace = fireplace
if not self._api.is_polling_in_background:
LOGGER.info("Starting Intellifire Background Polling Loop")
await self._api.start_background_polling()
# Don't return uninitialized poll data
async with asyncio.timeout(15):
try:
await self._api.poll()
except (ConnectionError, ClientConnectionError) as exception:
raise UpdateFailed from exception
LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts)
if self._api.failed_poll_attempts > 10:
LOGGER.debug("Too many polling errors - raising exception")
raise UpdateFailed
return self._api.data
@property @property
def read_api(self) -> IntellifireAPILocal: def read_api(self) -> IntelliFireDataProvider:
"""Return the Status API pointer.""" """Return the Status API pointer."""
return self._api return self.fireplace.read_api
@property @property
def control_api(self) -> IntellifireAPILocal: def control_api(self) -> IntelliFireController:
"""Return the control API.""" """Return the control API."""
return self._api return self.fireplace.control_api
async def _async_update_data(self) -> IntelliFirePollData:
return self.fireplace.data
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
@ -69,7 +54,6 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
manufacturer="Hearth and Home", manufacturer="Hearth and Home",
model="IFT-WFM", model="IFT-WFM",
name="IntelliFire", name="IntelliFire",
identifiers={("IntelliFire", f"{self.read_api.data.serial}]")}, identifiers={("IntelliFire", str(self.fireplace.serial))},
sw_version=self.read_api.data.fw_ver_str, configuration_url=f"http://{self.fireplace.ip_address}/poll",
configuration_url=f"http://{self._api.fireplace_ip}/poll",
) )

View File

@ -9,7 +9,7 @@ from . import IntellifireDataUpdateCoordinator
class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]): class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
"""Define a generic class for Intellifire entities.""" """Define a generic class for IntelliFire entities."""
_attr_attribution = "Data provided by unpublished Intellifire API" _attr_attribution = "Data provided by unpublished Intellifire API"
_attr_has_entity_name = True _attr_has_entity_name = True
@ -22,6 +22,8 @@ class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
"""Class initializer.""" """Class initializer."""
super().__init__(coordinator=coordinator) super().__init__(coordinator=coordinator)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{description.key}_{coordinator.read_api.data.serial}" self._attr_unique_id = f"{description.key}_{coordinator.fireplace.serial}"
self.identifiers = ({("IntelliFire", f"{coordinator.fireplace.serial}]")},)
# Configure the Device Info # Configure the Device Info
self._attr_device_info = self.coordinator.device_info self._attr_device_info = self.coordinator.device_info

View File

@ -7,7 +7,8 @@ from dataclasses import dataclass
import math import math
from typing import Any from typing import Any
from intellifire4py import IntellifireControlAsync, IntellifirePollData from intellifire4py.control import IntelliFireController
from intellifire4py.model import IntelliFirePollData
from homeassistant.components.fan import ( from homeassistant.components.fan import (
FanEntity, FanEntity,
@ -31,8 +32,8 @@ from .entity import IntellifireEntity
class IntellifireFanRequiredKeysMixin: class IntellifireFanRequiredKeysMixin:
"""Required keys for fan entity.""" """Required keys for fan entity."""
set_fn: Callable[[IntellifireControlAsync, int], Awaitable] set_fn: Callable[[IntelliFireController, int], Awaitable]
value_fn: Callable[[IntellifirePollData], bool] value_fn: Callable[[IntelliFirePollData], int]
speed_range: tuple[int, int] speed_range: tuple[int, int]
@ -91,7 +92,8 @@ class IntellifireFan(IntellifireEntity, FanEntity):
def percentage(self) -> int | None: def percentage(self) -> int | None:
"""Return fan percentage.""" """Return fan percentage."""
return ranged_value_to_percentage( return ranged_value_to_percentage(
self.entity_description.speed_range, self.coordinator.read_api.data.fanspeed self.entity_description.speed_range,
self.coordinator.read_api.data.fanspeed,
) )
@property @property

View File

@ -6,7 +6,8 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from intellifire4py import IntellifireControlAsync, IntellifirePollData from intellifire4py.control import IntelliFireController
from intellifire4py.model import IntelliFirePollData
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -27,8 +28,8 @@ from .entity import IntellifireEntity
class IntellifireLightRequiredKeysMixin: class IntellifireLightRequiredKeysMixin:
"""Required keys for fan entity.""" """Required keys for fan entity."""
set_fn: Callable[[IntellifireControlAsync, int], Awaitable] set_fn: Callable[[IntelliFireController, int], Awaitable]
value_fn: Callable[[IntellifirePollData], bool] value_fn: Callable[[IntelliFirePollData], int]
@dataclass(frozen=True) @dataclass(frozen=True)
@ -56,7 +57,7 @@ class IntellifireLight(IntellifireEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@property @property
def brightness(self): def brightness(self) -> int:
"""Return the current brightness 0-255.""" """Return the current brightness 0-255."""
return 85 * self.entity_description.value_fn(self.coordinator.read_api.data) return 85 * self.entity_description.value_fn(self.coordinator.read_api.data)

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/intellifire", "documentation": "https://www.home-assistant.io/integrations/intellifire",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["intellifire4py"], "loggers": ["intellifire4py"],
"requirements": ["intellifire4py==2.2.2"] "requirements": ["intellifire4py==4.1.9"]
} }

View File

@ -6,8 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from intellifire4py import IntellifirePollData
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@ -29,7 +27,9 @@ from .entity import IntellifireEntity
class IntellifireSensorRequiredKeysMixin: class IntellifireSensorRequiredKeysMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
value_fn: Callable[[IntellifirePollData], int | str | datetime | None] value_fn: Callable[
[IntellifireDataUpdateCoordinator], int | str | datetime | float | None
]
@dataclass(frozen=True) @dataclass(frozen=True)
@ -40,16 +40,29 @@ class IntellifireSensorEntityDescription(
"""Describes a sensor entity.""" """Describes a sensor entity."""
def _time_remaining_to_timestamp(data: IntellifirePollData) -> datetime | None: def _time_remaining_to_timestamp(
coordinator: IntellifireDataUpdateCoordinator,
) -> datetime | None:
"""Define a sensor that takes into account timezone.""" """Define a sensor that takes into account timezone."""
if not (seconds_offset := data.timeremaining_s): if not (seconds_offset := coordinator.data.timeremaining_s):
return None return None
return utcnow() + timedelta(seconds=seconds_offset) return utcnow() + timedelta(seconds=seconds_offset)
def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None: def _downtime_to_timestamp(
coordinator: IntellifireDataUpdateCoordinator,
) -> datetime | None:
"""Define a sensor that takes into account a timezone.""" """Define a sensor that takes into account a timezone."""
if not (seconds_offset := data.downtime): if not (seconds_offset := coordinator.data.downtime):
return None
return utcnow() - timedelta(seconds=seconds_offset)
def _uptime_to_timestamp(
coordinator: IntellifireDataUpdateCoordinator,
) -> datetime | None:
"""Return a timestamp of how long the sensor has been up."""
if not (seconds_offset := coordinator.data.uptime):
return None return None
return utcnow() - timedelta(seconds=seconds_offset) return utcnow() - timedelta(seconds=seconds_offset)
@ -60,14 +73,14 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
translation_key="flame_height", translation_key="flame_height",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
# UI uses 1-5 for flame height, backing lib uses 0-4 # UI uses 1-5 for flame height, backing lib uses 0-4
value_fn=lambda data: (data.flameheight + 1), value_fn=lambda coordinator: (coordinator.data.flameheight + 1),
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="temperature", key="temperature",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: data.temperature_c, value_fn=lambda coordinator: coordinator.data.temperature_c,
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="target_temp", key="target_temp",
@ -75,13 +88,13 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: data.thermostat_setpoint_c, value_fn=lambda coordinator: coordinator.data.thermostat_setpoint_c,
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="fan_speed", key="fan_speed",
translation_key="fan_speed", translation_key="fan_speed",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.fanspeed, value_fn=lambda coordinator: coordinator.data.fanspeed,
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="timer_end_timestamp", key="timer_end_timestamp",
@ -102,27 +115,27 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
translation_key="uptime", translation_key="uptime",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.TIMESTAMP, device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: utcnow() - timedelta(seconds=data.uptime), value_fn=_uptime_to_timestamp,
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="connection_quality", key="connection_quality",
translation_key="connection_quality", translation_key="connection_quality",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.connection_quality, value_fn=lambda coordinator: coordinator.data.connection_quality,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="ecm_latency", key="ecm_latency",
translation_key="ecm_latency", translation_key="ecm_latency",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.ecm_latency, value_fn=lambda coordinator: coordinator.data.ecm_latency,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
IntellifireSensorEntityDescription( IntellifireSensorEntityDescription(
key="ipv4_address", key="ipv4_address",
translation_key="ipv4_address", translation_key="ipv4_address",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.ipv4_address, value_fn=lambda coordinator: coordinator.data.ipv4_address,
), ),
) )
@ -134,17 +147,17 @@ async def async_setup_entry(
coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities(
IntellifireSensor(coordinator=coordinator, description=description) IntelliFireSensor(coordinator=coordinator, description=description)
for description in INTELLIFIRE_SENSORS for description in INTELLIFIRE_SENSORS
) )
class IntellifireSensor(IntellifireEntity, SensorEntity): class IntelliFireSensor(IntellifireEntity, SensorEntity):
"""Extends IntellifireEntity with Sensor specific logic.""" """Extends IntelliFireEntity with Sensor specific logic."""
entity_description: IntellifireSensorEntityDescription entity_description: IntellifireSensorEntityDescription
@property @property
def native_value(self) -> int | str | datetime | None: def native_value(self) -> int | str | datetime | float | None:
"""Return the state.""" """Return the state."""
return self.entity_description.value_fn(self.coordinator.read_api.data) return self.entity_description.value_fn(self.coordinator)

View File

@ -1,39 +1,30 @@
{ {
"config": { "config": {
"flow_title": "{serial} ({host})", "flow_title": "{serial}",
"step": { "step": {
"manual_device_entry": { "pick_cloud_device": {
"description": "Local Configuration", "title": "Configure fireplace",
"data": { "description": "Select fireplace by serial number:"
"host": "Host (IP Address)"
}
}, },
"api_config": { "cloud_api": {
"description": "Authenticate against IntelliFire Cloud",
"data_description": {
"username": "Your IntelliFire app username",
"password": "Your IntelliFire app password"
},
"data": { "data": {
"username": "[%key:common::config_flow::data::email%]", "username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
} }
},
"dhcp_confirm": {
"description": "Do you want to set up {host}\nSerial: {serial}?"
},
"pick_device": {
"title": "Device Selection",
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
} }
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "api_error": "Login failed"
"api_error": "Login failed",
"iftapi_connect": "Error conecting to iftapi.net"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"not_intellifire_device": "Not an IntelliFire Device." "not_intellifire_device": "Not an IntelliFire device.",
"no_available_devices": "All available devices have already been configured."
} }
}, },
"entity": { "entity": {

View File

@ -6,16 +6,13 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from intellifire4py import IntellifirePollData
from intellifire4py.intellifire import IntellifireAPILocal
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import IntellifireDataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .coordinator import IntellifireDataUpdateCoordinator
from .entity import IntellifireEntity from .entity import IntellifireEntity
@ -23,9 +20,9 @@ from .entity import IntellifireEntity
class IntellifireSwitchRequiredKeysMixin: class IntellifireSwitchRequiredKeysMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
on_fn: Callable[[IntellifireAPILocal], Awaitable] on_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable]
off_fn: Callable[[IntellifireAPILocal], Awaitable] off_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable]
value_fn: Callable[[IntellifirePollData], bool] value_fn: Callable[[IntellifireDataUpdateCoordinator], bool]
@dataclass(frozen=True) @dataclass(frozen=True)
@ -39,16 +36,16 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = (
IntellifireSwitchEntityDescription( IntellifireSwitchEntityDescription(
key="on_off", key="on_off",
translation_key="flame", translation_key="flame",
on_fn=lambda control_api: control_api.flame_on(), on_fn=lambda coordinator: coordinator.control_api.flame_on(),
off_fn=lambda control_api: control_api.flame_off(), off_fn=lambda coordinator: coordinator.control_api.flame_off(),
value_fn=lambda data: data.is_on, value_fn=lambda coordinator: coordinator.read_api.data.is_on,
), ),
IntellifireSwitchEntityDescription( IntellifireSwitchEntityDescription(
key="pilot", key="pilot",
translation_key="pilot_light", translation_key="pilot_light",
on_fn=lambda control_api: control_api.pilot_on(), on_fn=lambda coordinator: coordinator.control_api.pilot_on(),
off_fn=lambda control_api: control_api.pilot_off(), off_fn=lambda coordinator: coordinator.control_api.pilot_off(),
value_fn=lambda data: data.pilot_on, value_fn=lambda coordinator: coordinator.read_api.data.pilot_on,
), ),
) )
@ -74,15 +71,15 @@ class IntellifireSwitch(IntellifireEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch.""" """Turn on the switch."""
await self.entity_description.on_fn(self.coordinator.control_api) await self.entity_description.on_fn(self.coordinator)
await self.async_update_ha_state(force_refresh=True) await self.async_update_ha_state(force_refresh=True)
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch.""" """Turn off the switch."""
await self.entity_description.off_fn(self.coordinator.control_api) await self.entity_description.off_fn(self.coordinator)
await self.async_update_ha_state(force_refresh=True) await self.async_update_ha_state(force_refresh=True)
@property @property
def is_on(self) -> bool | None: def is_on(self) -> bool | None:
"""Return the on state.""" """Return the on state."""
return self.entity_description.value_fn(self.coordinator.read_api.data) return self.entity_description.value_fn(self.coordinator)

View File

@ -1179,7 +1179,7 @@ inkbird-ble==0.5.8
insteon-frontend-home-assistant==0.5.0 insteon-frontend-home-assistant==0.5.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==2.2.2 intellifire4py==4.1.9
# homeassistant.components.iotty # homeassistant.components.iotty
iottycloud==0.1.3 iottycloud==0.1.3

View File

@ -987,7 +987,7 @@ inkbird-ble==0.5.8
insteon-frontend-home-assistant==0.5.0 insteon-frontend-home-assistant==0.5.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==2.2.2 intellifire4py==4.1.9
# homeassistant.components.iotty # homeassistant.components.iotty
iottycloud==0.1.3 iottycloud==0.1.3

View File

@ -1 +1,13 @@
"""Tests for the IntelliFire integration.""" """Tests for the IntelliFire integration."""
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)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -1,14 +1,40 @@
"""Fixtures for IntelliFire integration tests.""" """Fixtures for IntelliFire integration tests."""
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
from aiohttp.client_reqrep import ConnectionKey from intellifire4py.const import IntelliFireApiMode
from intellifire4py.model import (
IntelliFireCommonFireplaceData,
IntelliFirePollData,
IntelliFireUserData,
)
import pytest import pytest
from homeassistant.components.intellifire.const import (
API_MODE_CLOUD,
API_MODE_LOCAL,
CONF_AUTH_COOKIE,
CONF_CONTROL_MODE,
CONF_READ_MODE,
CONF_SERIAL,
CONF_USER_ID,
CONF_WEB_CLIENT_ID,
DOMAIN,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_USERNAME,
)
from tests.common import MockConfigEntry, load_json_object_fixture
@pytest.fixture @pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]: def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Mock setting up a config entry.""" """Mock setting up a config entry."""
with patch( with patch(
"homeassistant.components.intellifire.async_setup_entry", return_value=True "homeassistant.components.intellifire.async_setup_entry", return_value=True
@ -17,44 +43,206 @@ def mock_setup_entry() -> Generator[AsyncMock]:
@pytest.fixture @pytest.fixture
def mock_fireplace_finder_none() -> Generator[MagicMock]: def mock_fireplace_finder_none() -> Generator[None, MagicMock, None]:
"""Mock fireplace finder.""" """Mock fireplace finder."""
mock_found_fireplaces = Mock() mock_found_fireplaces = Mock()
mock_found_fireplaces.ips = [] mock_found_fireplaces.ips = []
with patch( with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" "homeassistant.components.intellifire.config_flow.UDPFireplaceFinder.search_fireplace"
): ):
yield mock_found_fireplaces yield mock_found_fireplaces
@pytest.fixture @pytest.fixture
def mock_fireplace_finder_single() -> Generator[MagicMock]: def mock_config_entry_current() -> MockConfigEntry:
"""Mock fireplace finder.""" """Return a mock config entry."""
mock_found_fireplaces = Mock() return MockConfigEntry(
mock_found_fireplaces.ips = ["192.168.1.69"] domain=DOMAIN,
with patch( version=1,
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace" minor_version=2,
): data={
yield mock_found_fireplaces CONF_IP_ADDRESS: "192.168.2.108",
CONF_USERNAME: "grumpypanda@china.cn",
CONF_PASSWORD: "you-stole-my-pandas",
CONF_SERIAL: "3FB284769E4736F30C8973A7ED358123",
CONF_WEB_CLIENT_ID: "FA2B1C3045601234D0AE17D72F8E975",
CONF_API_KEY: "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
CONF_AUTH_COOKIE: "B984F21A6378560019F8A1CDE41B6782",
CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
},
options={CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_CLOUD},
unique_id="3FB284769E4736F30C8973A7ED358123",
)
@pytest.fixture @pytest.fixture
def mock_intellifire_config_flow() -> Generator[MagicMock]: def mock_config_entry_old() -> MockConfigEntry:
"""Return a mocked IntelliFire client.""" """For migration testing."""
data_mock = Mock() return MockConfigEntry(
data_mock.serial = "12345" domain=DOMAIN,
version=1,
minor_version=1,
title="Fireplace 3FB284769E4736F30C8973A7ED358123",
data={
CONF_HOST: "192.168.2.108",
CONF_USERNAME: "grumpypanda@china.cn",
CONF_PASSWORD: "you-stole-my-pandas",
CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
},
)
@pytest.fixture
def mock_common_data_local() -> IntelliFireCommonFireplaceData:
"""Fixture for mock common data."""
return IntelliFireCommonFireplaceData(
auth_cookie="B984F21A6378560019F8A1CDE41B6782",
user_id="52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
web_client_id="FA2B1C3045601234D0AE17D72F8E975",
serial="3FB284769E4736F30C8973A7ED358123",
api_key="B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
ip_address="192.168.2.108",
read_mode=IntelliFireApiMode.LOCAL,
control_mode=IntelliFireApiMode.LOCAL,
)
@pytest.fixture
def mock_apis_multifp(
mock_cloud_interface, mock_local_interface, mock_fp
) -> Generator[tuple[AsyncMock, AsyncMock, MagicMock], None, None]:
"""Multi fireplace version of mocks."""
return mock_local_interface, mock_cloud_interface, mock_fp
@pytest.fixture
def mock_apis_single_fp(
mock_cloud_interface, mock_local_interface, mock_fp
) -> Generator[tuple[AsyncMock, AsyncMock, MagicMock], None, None]:
"""Single fire place version of the mocks."""
data_v1 = IntelliFireUserData(
**load_json_object_fixture("user_data_1.json", DOMAIN)
)
with patch.object(
type(mock_cloud_interface), "user_data", new_callable=PropertyMock
) as mock_user_data:
mock_user_data.return_value = data_v1
yield mock_local_interface, mock_cloud_interface, mock_fp
@pytest.fixture
def mock_cloud_interface() -> Generator[AsyncMock, None, None]:
"""Mock cloud interface to use for testing."""
user_data = IntelliFireUserData(
**load_json_object_fixture("user_data_3.json", DOMAIN)
)
with (
patch(
"homeassistant.components.intellifire.IntelliFireCloudInterface",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.intellifire.config_flow.IntelliFireCloudInterface",
new=mock_client,
),
patch(
"intellifire4py.cloud_interface.IntelliFireCloudInterface",
new=mock_client,
),
):
# Mock async context manager
mock_client = mock_client.return_value
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=None)
# Mock other async methods if needed
mock_client.login_with_credentials = AsyncMock()
mock_client.poll = AsyncMock()
type(mock_client).user_data = PropertyMock(return_value=user_data)
yield mock_client # Yielding to the test
@pytest.fixture
def mock_local_interface() -> Generator[AsyncMock, None, None]:
"""Mock version of IntelliFireAPILocal."""
poll_data = IntelliFirePollData(
**load_json_object_fixture("intellifire/local_poll.json")
)
with patch( with patch(
"homeassistant.components.intellifire.config_flow.IntellifireAPILocal", "homeassistant.components.intellifire.config_flow.IntelliFireAPILocal",
autospec=True, autospec=True,
) as intellifire_mock: ) as mock_client:
intellifire = intellifire_mock.return_value mock_client = mock_client.return_value
intellifire.data = data_mock # Mock all instances of the class
yield intellifire type(mock_client).data = PropertyMock(return_value=poll_data)
yield mock_client
def mock_api_connection_error() -> ConnectionError: @pytest.fixture
"""Return a fake a ConnectionError for iftapi.net.""" def mock_fp(mock_common_data_local) -> Generator[AsyncMock, None, None]:
ret = ConnectionError() """Mock fireplace."""
ret.args = [ConnectionKey("iftapi.net", 443, False, None, None, None, None)]
return ret local_poll_data = IntelliFirePollData(
**load_json_object_fixture("local_poll.json", DOMAIN)
)
assert local_poll_data.connection_quality == 988451
with patch(
"homeassistant.components.intellifire.UnifiedFireplace"
) as mock_unified_fireplace:
# Create an instance of the mock
mock_instance = mock_unified_fireplace.return_value
# Mock methods and properties of the instance
mock_instance.perform_cloud_poll = AsyncMock()
mock_instance.perform_local_poll = AsyncMock()
mock_instance.async_validate_connectivity = AsyncMock(return_value=(True, True))
type(mock_instance).is_cloud_polling = PropertyMock(return_value=False)
type(mock_instance).is_local_polling = PropertyMock(return_value=True)
mock_instance.get_user_data_as_json.return_value = '{"mock": "data"}'
mock_instance.ip_address = "192.168.1.100"
mock_instance.api_key = "mock_api_key"
mock_instance.serial = "mock_serial"
mock_instance.user_id = "mock_user_id"
mock_instance.auth_cookie = "mock_auth_cookie"
mock_instance.web_client_id = "mock_web_client_id"
# Configure the READ Api
mock_instance.read_api = MagicMock()
mock_instance.read_api.poll = MagicMock(return_value=local_poll_data)
mock_instance.read_api.data = local_poll_data
mock_instance.control_api = MagicMock()
mock_instance.local_connectivity = True
mock_instance.cloud_connectivity = False
mock_instance._read_mode = IntelliFireApiMode.LOCAL
mock_instance.read_mode = IntelliFireApiMode.LOCAL
mock_instance.control_mode = IntelliFireApiMode.LOCAL
mock_instance._control_mode = IntelliFireApiMode.LOCAL
mock_instance.data = local_poll_data
mock_instance.set_read_mode = AsyncMock()
mock_instance.set_control_mode = AsyncMock()
mock_instance.async_validate_connectivity = AsyncMock(
return_value=(True, False)
)
# Patch class methods
with patch(
"homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common",
new_callable=AsyncMock,
return_value=mock_instance,
):
yield mock_instance

View File

@ -0,0 +1,29 @@
{
"name": "",
"serial": "4GC295860E5837G40D9974B7FD459234",
"temperature": 17,
"battery": 0,
"pilot": 1,
"light": 0,
"height": 1,
"fanspeed": 1,
"hot": 0,
"power": 1,
"thermostat": 0,
"setpoint": 0,
"timer": 0,
"timeremaining": 0,
"prepurge": 0,
"feature_light": 0,
"feature_thermostat": 1,
"power_vent": 0,
"feature_fan": 1,
"errors": [],
"fw_version": "0x00030200",
"fw_ver_str": "0.3.2+hw2",
"downtime": 0,
"uptime": 117,
"connection_quality": 988451,
"ecm_latency": 0,
"ipv4_address": "192.168.2.108"
}

View File

@ -0,0 +1,17 @@
{
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"fireplaces": [
{
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"ip_address": "192.168.2.108",
"api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
"serial": "3FB284769E4736F30C8973A7ED358123"
}
],
"username": "grumpypanda@china.cn",
"password": "you-stole-my-pandas"
}

View File

@ -0,0 +1,33 @@
{
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"fireplaces": [
{
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"ip_address": "192.168.2.108",
"api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
"serial": "3FB284769E4736F30C8973A7ED358123"
},
{
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"ip_address": "192.168.2.109",
"api_key": "D4C5EB28BBFF41E1FB21AFF9BFA6CD34",
"serial": "4GC295860E5837G40D9974B7FD459234"
},
{
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"ip_address": "192.168.2.110",
"api_key": "E5D6FC39CCED52F1FB21AFF9BFA6DE56",
"serial": "5HD306971F5938H51EAA85C8GE561345"
}
],
"username": "grumpypanda@china.cn",
"password": "you-stole-my-pandas"
}

View File

@ -0,0 +1,717 @@
# serializer version: 1
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_accessory_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_accessory_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Accessory error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'accessory_error',
'unique_id': 'error_accessory_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_accessory_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Accessory error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_accessory_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_disabled_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_disabled_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Disabled error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'disabled_error',
'unique_id': 'error_disabled_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_disabled_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Disabled error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_disabled_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_ecm_offline_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_ecm_offline_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'ECM offline error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'ecm_offline_error',
'unique_id': 'error_ecm_offline_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_ecm_offline_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire ECM offline error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_ecm_offline_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_delay_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_fan_delay_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Fan delay error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'fan_delay_error',
'unique_id': 'error_fan_delay_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_delay_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Fan delay error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_fan_delay_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_fan_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Fan error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'fan_error',
'unique_id': 'error_fan_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_fan_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Fan error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_fan_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.intellifire_flame',
'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': 'Flame',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'flame',
'unique_id': 'on_off_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Flame',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_flame',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_flame_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Flame Error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'flame_error',
'unique_id': 'error_flame_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_flame_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Flame Error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_flame_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_lights_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_lights_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Lights error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lights_error',
'unique_id': 'error_lights_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_lights_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Lights error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_lights_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_maintenance_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_maintenance_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Maintenance error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'maintenance_error',
'unique_id': 'error_maintenance_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_maintenance_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Maintenance error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_maintenance_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_offline_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_offline_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Offline error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'offline_error',
'unique_id': 'error_offline_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_offline_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Offline error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_offline_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_flame_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_pilot_flame_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Pilot flame error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pilot_flame_error',
'unique_id': 'error_pilot_flame_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_flame_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Pilot flame error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_pilot_flame_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_light_on-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.intellifire_pilot_light_on',
'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': 'Pilot light on',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'pilot_light_on',
'unique_id': 'pilot_light_on_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_pilot_light_on-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Pilot light on',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_pilot_light_on',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_soft_lock_out_error-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.intellifire_soft_lock_out_error',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'Soft lock out error',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'soft_lock_out_error',
'unique_id': 'error_soft_lock_out_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_soft_lock_out_error-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'problem',
'friendly_name': 'IntelliFire Soft lock out error',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_soft_lock_out_error',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_thermostat_on-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.intellifire_thermostat_on',
'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': 'Thermostat on',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'thermostat_on',
'unique_id': 'thermostat_on_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_thermostat_on-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Thermostat on',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_thermostat_on',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_timer_on-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.intellifire_timer_on',
'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': 'Timer on',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'timer_on',
'unique_id': 'timer_on_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_binary_sensor_entities[binary_sensor.intellifire_timer_on-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Timer on',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.intellifire_timer_on',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,66 @@
# serializer version: 1
# name: test_all_sensor_entities[climate.intellifire_thermostat-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'hvac_modes': list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 37,
'min_temp': 0,
'target_temp_step': 1.0,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'climate',
'entity_category': None,
'entity_id': 'climate.intellifire_thermostat',
'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': 'Thermostat',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': <ClimateEntityFeature: 385>,
'translation_key': None,
'unique_id': 'climate_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[climate.intellifire_thermostat-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'current_temperature': 17.0,
'friendly_name': 'IntelliFire Thermostat',
'hvac_modes': list([
<HVACMode.HEAT: 'heat'>,
<HVACMode.OFF: 'off'>,
]),
'max_temp': 37,
'min_temp': 0,
'supported_features': <ClimateEntityFeature: 385>,
'target_temp_step': 1.0,
'temperature': 0.0,
}),
'context': <ANY>,
'entity_id': 'climate.intellifire_thermostat',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,587 @@
# serializer version: 1
# name: test_all_sensor_entities[sensor.intellifire_connection_quality-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_connection_quality',
'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': 'Connection quality',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'connection_quality',
'unique_id': 'connection_quality_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_connection_quality-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Connection quality',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_connection_quality',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '988451',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_downtime-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_downtime',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Downtime',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'downtime',
'unique_id': 'downtime_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_downtime-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'timestamp',
'friendly_name': 'IntelliFire Downtime',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_downtime',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_ecm_latency-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_ecm_latency',
'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': 'ECM latency',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'ecm_latency',
'unique_id': 'ecm_latency_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_ecm_latency-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire ECM latency',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_ecm_latency',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_fan_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.intellifire_fan_speed',
'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': 'Fan Speed',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'fan_speed',
'unique_id': 'fan_speed_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_fan_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Fan Speed',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_fan_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_flame_height-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.intellifire_flame_height',
'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': 'Flame height',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'flame_height',
'unique_id': 'flame_height_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_flame_height-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Flame height',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_flame_height',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_ip_address-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_ip_address',
'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': 'IP address',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'ipv4_address',
'unique_id': 'ipv4_address_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_ip_address-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire IP address',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_ip_address',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '192.168.2.108',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_local_connectivity-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_local_connectivity',
'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': 'Local connectivity',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'local_connectivity',
'unique_id': 'local_connectivity_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_local_connectivity-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire Local connectivity',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_local_connectivity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'True',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_none-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_none',
'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': None,
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'local_connectivity',
'unique_id': 'local_connectivity_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_none-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'friendly_name': 'IntelliFire None',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_none',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'True',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_target_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.intellifire_target_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Target temperature',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'target_temp',
'unique_id': 'target_temp_mock_serial',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_target_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'temperature',
'friendly_name': 'IntelliFire Target temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_target_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.intellifire_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'temperature_mock_serial',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'temperature',
'friendly_name': 'IntelliFire Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '17',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_timer_end-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.intellifire_timer_end',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Timer end',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'timer_end_timestamp',
'unique_id': 'timer_end_timestamp_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_timer_end-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'timestamp',
'friendly_name': 'IntelliFire Timer end',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_timer_end',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_uptime-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.intellifire_uptime',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
'original_icon': None,
'original_name': 'Uptime',
'platform': 'intellifire',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'uptime',
'unique_id': 'uptime_mock_serial',
'unit_of_measurement': None,
})
# ---
# name: test_all_sensor_entities[sensor.intellifire_uptime-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by unpublished Intellifire API',
'device_class': 'timestamp',
'friendly_name': 'IntelliFire Uptime',
}),
'context': <ANY>,
'entity_id': 'sensor.intellifire_uptime',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2021-01-01T11:58:03+00:00',
})
# ---

View File

@ -0,0 +1,35 @@
"""Test IntelliFire Binary Sensors."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_binary_sensor_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_config_entry_current: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_apis_single_fp: tuple[AsyncMock, AsyncMock, AsyncMock],
) -> None:
"""Test all entities."""
with (
patch(
"homeassistant.components.intellifire.PLATFORMS", [Platform.BINARY_SENSOR]
),
):
await setup_integration(hass, mock_config_entry_current)
await snapshot_platform(
hass, entity_registry, snapshot, mock_config_entry_current.entry_id
)

View File

@ -0,0 +1,34 @@
"""Test climate."""
from unittest.mock import patch
from freezegun import freeze_time
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@freeze_time("2021-01-01T12:00:00Z")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_sensor_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_config_entry_current: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_fp,
) -> None:
"""Test all entities."""
with (
patch("homeassistant.components.intellifire.PLATFORMS", [Platform.CLIMATE]),
):
await setup_integration(hass, mock_config_entry_current)
await snapshot_platform(
hass, entity_registry, snapshot, mock_config_entry_current.entry_id
)

View File

@ -1,323 +1,168 @@
"""Test the IntelliFire config flow.""" """Test the IntelliFire config flow."""
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock
from intellifire4py.exceptions import LoginException from intellifire4py.exceptions import LoginError
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import dhcp from homeassistant.components import dhcp
from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import CONF_SERIAL, DOMAIN
from homeassistant.components.intellifire.const import CONF_USER_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from .conftest import mock_api_connection_error
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@patch.multiple( async def test_standard_config_with_single_fireplace(
"homeassistant.components.intellifire.config_flow.IntellifireAPICloud",
login=AsyncMock(),
get_user_id=MagicMock(return_value="intellifire"),
get_fireplace_api_key=MagicMock(return_value="key"),
)
async def test_no_discovery(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock, mock_apis_single_fp,
) -> None: ) -> None:
"""Test we should get the manual discovery form - because no discovered fireplaces.""" """Test standard flow with a user who has only a single fireplace."""
with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", result = await hass.config_entries.flow.async_init(
return_value=[], DOMAIN, context={"source": config_entries.SOURCE_USER}
): )
result = await hass.config_entries.flow.async_init( assert result["type"] == FlowResultType.FORM
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {} assert result["errors"] == {}
assert result["step_id"] == "manual_device_entry" assert result["step_id"] == "cloud_api"
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
CONF_HOST: "1.1.1.1",
},
) )
await hass.async_block_till_done() # For a single fireplace we just create it
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result2["type"] is FlowResultType.FORM assert result["data"] == {
assert result2["step_id"] == "api_config" "ip_address": "192.168.2.108",
"api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
result3 = await hass.config_entries.flow.async_configure( "serial": "3FB284769E4736F30C8973A7ED358123",
result["flow_id"], "auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, "web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
) "user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
await hass.async_block_till_done() "username": "grumpypanda@china.cn",
"password": "you-stole-my-pandas",
assert result3["type"] is FlowResultType.CREATE_ENTRY
assert result3["title"] == "Fireplace 12345"
assert result3["data"] == {
CONF_HOST: "1.1.1.1",
CONF_USERNAME: "test",
CONF_PASSWORD: "AROONIE",
CONF_API_KEY: "key",
CONF_USER_ID: "intellifire",
} }
assert len(mock_setup_entry.mock_calls) == 1
@patch.multiple( async def test_standard_config_with_pre_configured_fireplace(
"homeassistant.components.intellifire.config_flow.IntellifireAPICloud",
login=AsyncMock(side_effect=mock_api_connection_error()),
get_user_id=MagicMock(return_value="intellifire"),
get_fireplace_api_key=MagicMock(return_value="key"),
)
async def test_single_discovery(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock, mock_config_entry_current,
mock_apis_single_fp,
) -> None: ) -> None:
"""Test single fireplace UDP discovery.""" """What if we try to configure an already configured fireplace."""
with patch( # Configure an existing entry
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", mock_config_entry_current.add_to_hass(hass)
return_value=["192.168.1.69"],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_init(
result["flow_id"], {CONF_HOST: "192.168.1.69"} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM
result3 = await hass.config_entries.flow.async_configure( assert result["errors"] == {}
assert result["step_id"] == "cloud_api"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
) )
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.FORM # For a single fireplace we just create it
assert result3["errors"] == {"base": "iftapi_connect"} assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "no_available_devices"
@patch.multiple( async def test_standard_config_with_single_fireplace_and_bad_credentials(
"homeassistant.components.intellifire.config_flow.IntellifireAPICloud",
login=AsyncMock(side_effect=LoginException),
get_user_id=MagicMock(return_value="intellifire"),
get_fireplace_api_key=MagicMock(return_value="key"),
)
async def test_single_discovery_loign_error(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock, mock_apis_single_fp,
) -> None: ) -> None:
"""Test single fireplace UDP discovery.""" """Test bad credentials on a login."""
with patch( mock_local_interface, mock_cloud_interface, mock_fp = mock_apis_single_fp
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace", # Set login error
return_value=["192.168.1.69"], mock_cloud_interface.login_with_credentials.side_effect = LoginError
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: "192.168.1.69"}
)
await hass.async_block_till_done()
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"},
)
await hass.async_block_till_done()
assert result3["type"] is FlowResultType.FORM
assert result3["errors"] == {"base": "api_error"}
async def test_manual_entry(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock,
) -> None:
"""Test for multiple Fireplace discovery - involving a pick_device step."""
with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace",
return_value=["192.168.1.69", "192.168.1.33", "192.168.169"],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "pick_device"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_HOST: MANUAL_ENTRY_STRING}
)
await hass.async_block_till_done()
assert result2["step_id"] == "manual_device_entry"
async def test_multi_discovery(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock,
) -> None:
"""Test for multiple fireplace discovery - involving a pick_device step."""
with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace",
return_value=["192.168.1.69", "192.168.1.33", "192.168.169"],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["step_id"] == "pick_device"
await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_HOST: "192.168.1.33"}
)
await hass.async_block_till_done()
assert result["step_id"] == "pick_device"
async def test_multi_discovery_cannot_connect(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock,
) -> None:
"""Test for multiple fireplace discovery - involving a pick_device step."""
with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace",
return_value=["192.168.1.69", "192.168.1.33", "192.168.169"],
):
mock_intellifire_config_flow.poll.side_effect = ConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "pick_device"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_HOST: "192.168.1.33"}
)
await hass.async_block_till_done()
assert result2["type"] is FlowResultType.FORM
assert result2["errors"] == {"base": "cannot_connect"}
async def test_form_cannot_connect_manual_entry(
hass: HomeAssistant,
mock_intellifire_config_flow: MagicMock,
mock_fireplace_finder_single: AsyncMock,
) -> None:
"""Test we handle cannot connect error."""
mock_intellifire_config_flow.poll.side_effect = ConnectionError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "manual_device_entry" assert result["errors"] == {}
assert result["step_id"] == "cloud_api"
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
CONF_HOST: "1.1.1.1",
},
) )
assert result2["type"] is FlowResultType.FORM # Erase the error
assert result2["errors"] == {"base": "cannot_connect"} mock_cloud_interface.login_with_credentials.side_effect = None
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "api_error"}
assert result["step_id"] == "cloud_api"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
)
# For a single fireplace we just create it
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
"ip_address": "192.168.2.108",
"api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
"serial": "3FB284769E4736F30C8973A7ED358123",
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"username": "grumpypanda@china.cn",
"password": "you-stole-my-pandas",
}
async def test_picker_already_discovered( async def test_standard_config_with_multiple_fireplace(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock, mock_apis_multifp,
) -> None: ) -> None:
"""Test single fireplace UDP discovery.""" """Test multi-fireplace user who must be very rich."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "192.168.1.3",
},
title="Fireplace",
unique_id=44444,
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.intellifire.config_flow.AsyncUDPFireplaceFinder.search_fireplace",
return_value=["192.168.1.3"],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await hass.async_block_till_done()
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "192.168.1.4",
},
)
assert result2["type"] is FlowResultType.FORM
assert len(mock_setup_entry.mock_calls) == 0
@patch.multiple(
"homeassistant.components.intellifire.config_flow.IntellifireAPICloud",
login=AsyncMock(),
get_user_id=MagicMock(return_value="intellifire"),
get_fireplace_api_key=MagicMock(return_value="key"),
)
async def test_reauth_flow(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock,
) -> None:
"""Test the reauth flow."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"host": "192.168.1.3",
},
title="Fireplace 1234",
version=1,
unique_id="4444",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN, context={"source": config_entries.SOURCE_USER}
context={
"source": "reauth",
"unique_id": entry.unique_id,
"entry_id": entry.entry_id,
},
) )
assert result["type"] == FlowResultType.FORM
assert result["errors"] == {}
assert result["step_id"] == "cloud_api"
assert result["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(
assert result["step_id"] == "api_config"
result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
) )
await hass.async_block_till_done() # When we have multiple fireplaces we get to pick a serial
assert result3["type"] is FlowResultType.ABORT assert result["type"] == FlowResultType.FORM
assert entry.data[CONF_PASSWORD] == "AROONIE" assert result["step_id"] == "pick_cloud_device"
assert entry.data[CONF_USERNAME] == "test" result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_SERIAL: "4GC295860E5837G40D9974B7FD459234"},
)
assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["data"] == {
"ip_address": "192.168.2.109",
"api_key": "D4C5EB28BBFF41E1FB21AFF9BFA6CD34",
"serial": "4GC295860E5837G40D9974B7FD459234",
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"username": "grumpypanda@china.cn",
"password": "you-stole-my-pandas",
}
async def test_dhcp_discovery_intellifire_device( async def test_dhcp_discovery_intellifire_device(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_intellifire_config_flow: MagicMock, mock_apis_multifp,
) -> None: ) -> None:
"""Test successful DHCP Discovery.""" """Test successful DHCP Discovery."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_DHCP}, context={"source": config_entries.SOURCE_DHCP},
@ -327,26 +172,26 @@ async def test_dhcp_discovery_intellifire_device(
hostname="zentrios-Test", hostname="zentrios-Test",
), ),
) )
assert result["type"] is FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "dhcp_confirm" assert result["step_id"] == "cloud_api"
result2 = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result2["type"] is FlowResultType.FORM result = await hass.config_entries.flow.async_configure(
assert result2["step_id"] == "dhcp_confirm" result["flow_id"],
result3 = await hass.config_entries.flow.async_configure( {CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
result2["flow_id"], user_input={}
) )
assert result3["title"] == "Fireplace 12345" assert result["type"] == FlowResultType.CREATE_ENTRY
assert result3["data"] == {"host": "1.1.1.1"}
async def test_dhcp_discovery_non_intellifire_device( async def test_dhcp_discovery_non_intellifire_device(
hass: HomeAssistant, hass: HomeAssistant,
mock_intellifire_config_flow: MagicMock,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_apis_multifp,
) -> None: ) -> None:
"""Test failed DHCP Discovery.""" """Test successful DHCP Discovery of a non intellifire device.."""
mock_intellifire_config_flow.poll.side_effect = ConnectionError # Patch poll with an exception
mock_local_interface, mock_cloud_interface, mock_fp = mock_apis_multifp
mock_local_interface.poll.side_effect = ConnectionError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
@ -357,6 +202,28 @@ async def test_dhcp_discovery_non_intellifire_device(
hostname="zentrios-Evil", hostname="zentrios-Evil",
), ),
) )
assert result["type"] == FlowResultType.ABORT
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "not_intellifire_device" assert result["reason"] == "not_intellifire_device"
# Test is finished - the DHCP scanner detected a hostname that "might" be an IntelliFire device, but it was not.
async def test_reauth_flow(
hass: HomeAssistant,
mock_config_entry_current: MockConfigEntry,
mock_apis_single_fp,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reauth."""
mock_config_entry_current.add_to_hass(hass)
result = await mock_config_entry_current.start_reauth_flow(hass)
assert result["type"] == FlowResultType.FORM
result["step_id"] = "cloud_api"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: "donJulio", CONF_PASSWORD: "Tequila0FD00m"},
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "reauth_successful"

View File

@ -0,0 +1,111 @@
"""Test the IntelliFire config flow."""
from unittest.mock import AsyncMock, patch
from homeassistant.components.intellifire import CONF_USER_ID
from homeassistant.components.intellifire.const import (
API_MODE_CLOUD,
API_MODE_LOCAL,
CONF_AUTH_COOKIE,
CONF_CONTROL_MODE,
CONF_READ_MODE,
CONF_SERIAL,
CONF_WEB_CLIENT_ID,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def test_minor_migration(
hass: HomeAssistant, mock_config_entry_old, mock_apis_single_fp
) -> None:
"""With the new library we are going to end up rewriting the config entries."""
mock_config_entry_old.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_old.entry_id)
assert mock_config_entry_old.data == {
"ip_address": "192.168.2.108",
"host": "192.168.2.108",
"api_key": "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
"serial": "3FB284769E4736F30C8973A7ED358123",
"auth_cookie": "B984F21A6378560019F8A1CDE41B6782",
"web_client_id": "FA2B1C3045601234D0AE17D72F8E975",
"user_id": "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
"username": "grumpypanda@china.cn",
"password": "you-stole-my-pandas",
}
async def test_minor_migration_error(hass: HomeAssistant, mock_apis_single_fp) -> None:
"""Test the case where we completely fail to initialize."""
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
version=1,
minor_version=1,
title="Fireplace of testing",
data={
CONF_HOST: "11.168.2.218",
CONF_USERNAME: "grumpypanda@china.cn",
CONF_PASSWORD: "you-stole-my-pandas",
CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
},
)
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.MIGRATION_ERROR
async def test_init_with_no_username(hass: HomeAssistant, mock_apis_single_fp) -> None:
"""Test the case where we completely fail to initialize."""
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
version=1,
minor_version=2,
data={
CONF_IP_ADDRESS: "192.168.2.108",
CONF_PASSWORD: "you-stole-my-pandas",
CONF_SERIAL: "3FB284769E4736F30C8973A7ED358123",
CONF_WEB_CLIENT_ID: "FA2B1C3045601234D0AE17D72F8E975",
CONF_API_KEY: "B5C4DA27AAEF31D1FB21AFF9BFA6BCD2",
CONF_AUTH_COOKIE: "B984F21A6378560019F8A1CDE41B6782",
CONF_USER_ID: "52C3F9E8B9D3AC99F8E4D12345678901FE9A2BC7D85F7654E28BF98BCD123456",
},
options={CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_CLOUD},
unique_id="3FB284769E4736F30C8973A7ED358123",
)
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_ERROR
async def test_connectivity_bad(
hass: HomeAssistant,
mock_config_entry_current,
mock_apis_single_fp,
) -> None:
"""Test a timeout error on the setup flow."""
with patch(
"homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common",
new_callable=AsyncMock,
side_effect=TimeoutError,
):
mock_config_entry_current.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry_current.entry_id)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0

View File

@ -0,0 +1,35 @@
"""Test IntelliFire Binary Sensors."""
from unittest.mock import AsyncMock, patch
from freezegun import freeze_time
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@freeze_time("2021-01-01T12:00:00Z")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_sensor_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_config_entry_current: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_apis_single_fp: tuple[AsyncMock, AsyncMock, AsyncMock],
) -> None:
"""Test all entities."""
with (
patch("homeassistant.components.intellifire.PLATFORMS", [Platform.SENSOR]),
):
await setup_integration(hass, mock_config_entry_current)
await snapshot_platform(
hass, entity_registry, snapshot, mock_config_entry_current.entry_id
)