mirror of
https://github.com/home-assistant/core.git
synced 2025-08-03 12:45:28 +02:00
Merge branch 'dev' into jbouwh-mqtt-device-discovery
This commit is contained in:
@@ -1422,7 +1422,6 @@ omit =
|
||||
homeassistant/components/tensorflow/image_processing.py
|
||||
homeassistant/components/tfiac/climate.py
|
||||
homeassistant/components/thermoworks_smoke/sensor.py
|
||||
homeassistant/components/thethingsnetwork/*
|
||||
homeassistant/components/thingspeak/*
|
||||
homeassistant/components/thinkingcleaner/*
|
||||
homeassistant/components/thomson/device_tracker.py
|
||||
|
@@ -428,6 +428,7 @@ homeassistant.components.tcp.*
|
||||
homeassistant.components.technove.*
|
||||
homeassistant.components.tedee.*
|
||||
homeassistant.components.text.*
|
||||
homeassistant.components.thethingsnetwork.*
|
||||
homeassistant.components.threshold.*
|
||||
homeassistant.components.tibber.*
|
||||
homeassistant.components.tile.*
|
||||
|
@@ -1421,7 +1421,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/thermobeacon/ @bdraco
|
||||
/homeassistant/components/thermopro/ @bdraco @h3ss
|
||||
/tests/components/thermopro/ @bdraco @h3ss
|
||||
/homeassistant/components/thethingsnetwork/ @fabaff
|
||||
/homeassistant/components/thethingsnetwork/ @angelnu
|
||||
/tests/components/thethingsnetwork/ @angelnu
|
||||
/homeassistant/components/thread/ @home-assistant/core
|
||||
/tests/components/thread/ @home-assistant/core
|
||||
/homeassistant/components/tibber/ @danielhiversen
|
||||
|
@@ -2,12 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import timeout
|
||||
from functools import partial
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google.api_core.exceptions import ClientError
|
||||
from google.api_core.exceptions import ClientError, DeadlineExceeded, GoogleAPICallError
|
||||
import google.generativeai as genai
|
||||
import google.generativeai.types as genai_types
|
||||
import voluptuous as vol
|
||||
@@ -20,11 +19,16 @@ from homeassistant.core import (
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryError,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_PROMPT, DOMAIN, LOGGER
|
||||
from .const import CONF_CHAT_MODEL, CONF_PROMPT, DOMAIN, RECOMMENDED_CHAT_MODEL
|
||||
|
||||
SERVICE_GENERATE_CONTENT = "generate_content"
|
||||
CONF_IMAGE_FILENAME = "image_filename"
|
||||
@@ -101,13 +105,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
genai.configure(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
try:
|
||||
async with timeout(5.0):
|
||||
next(await hass.async_add_executor_job(partial(genai.list_models)), None)
|
||||
except (ClientError, TimeoutError) as err:
|
||||
await hass.async_add_executor_job(
|
||||
partial(
|
||||
genai.get_model,
|
||||
entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
request_options={"timeout": 5.0},
|
||||
)
|
||||
)
|
||||
except (GoogleAPICallError, ValueError) as err:
|
||||
if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID":
|
||||
LOGGER.error("Invalid API key: %s", err)
|
||||
return False
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
if isinstance(err, DeadlineExceeded):
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
raise ConfigEntryError(err) from err
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@@ -2,12 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from google.api_core.exceptions import ClientError
|
||||
from google.api_core.exceptions import ClientError, GoogleAPICallError
|
||||
import google.generativeai as genai
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -17,7 +18,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -54,7 +55,7 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
STEP_API_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
@@ -73,7 +74,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
genai.configure(api_key=data[CONF_API_KEY])
|
||||
await hass.async_add_executor_job(partial(genai.list_models))
|
||||
|
||||
def get_first_model():
|
||||
return next(genai.list_models(request_options={"timeout": 5.0}), None)
|
||||
|
||||
await hass.async_add_executor_job(partial(get_first_model))
|
||||
|
||||
|
||||
class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -81,36 +86,74 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a new GoogleGenerativeAIConfigFlow."""
|
||||
self.reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_api(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except GoogleAPICallError as err:
|
||||
if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID":
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
if self.reauth_entry:
|
||||
return self.async_update_reload_and_abort(
|
||||
self.reauth_entry,
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="Google Generative AI",
|
||||
data=user_input,
|
||||
options=RECOMMENDED_OPTIONS,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="api",
|
||||
data_schema=STEP_API_DATA_SCHEMA,
|
||||
description_placeholders={
|
||||
"api_key_url": "https://aistudio.google.com/app/apikey"
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
return await self.async_step_api()
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except ClientError as err:
|
||||
if err.reason == "API_KEY_INVALID":
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="Google Generative AI",
|
||||
data=user_input,
|
||||
options=RECOMMENDED_OPTIONS,
|
||||
)
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_api()
|
||||
assert self.reauth_entry
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={
|
||||
CONF_NAME: self.reauth_entry.title,
|
||||
CONF_API_KEY: self.reauth_entry.data.get(CONF_API_KEY, ""),
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@@ -221,7 +221,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
api_prompt = await llm_api.async_get_api_prompt(empty_tool_input)
|
||||
|
||||
else:
|
||||
api_prompt = llm.PROMPT_NO_API_CONFIGURED
|
||||
api_prompt = llm.async_render_no_api_prompt(self.hass)
|
||||
|
||||
prompt = "\n".join(
|
||||
(
|
||||
|
@@ -1,17 +1,24 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"api": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]"
|
||||
}
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"description": "Get your API key from [here]({api_key_url})."
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Your current API key: {api_key} is no longer valid. Please enter a new valid API key."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
@@ -27,10 +27,9 @@ from homeassistant.core import (
|
||||
HassJob,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
async_get_hass,
|
||||
async_get_hass_or_none,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
@@ -160,10 +159,7 @@ VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
|
||||
def valid_addon(value: Any) -> str:
|
||||
"""Validate value is a valid addon slug."""
|
||||
value = VALID_ADDON_SLUG(value)
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
hass = async_get_hass_or_none()
|
||||
|
||||
if hass and (addons := get_addons_info(hass)) is not None and value not in addons:
|
||||
raise vol.Invalid("Not a valid add-on slug")
|
||||
|
@@ -97,10 +97,14 @@ DATA_KEY_CORE = "core"
|
||||
DATA_KEY_HOST = "host"
|
||||
DATA_KEY_SUPERVISOR_ISSUES = "supervisor_issues"
|
||||
|
||||
PLACEHOLDER_KEY_ADDON = "addon"
|
||||
PLACEHOLDER_KEY_ADDON_URL = "addon_url"
|
||||
PLACEHOLDER_KEY_REFERENCE = "reference"
|
||||
PLACEHOLDER_KEY_COMPONENTS = "components"
|
||||
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG = "issue_system_docker_config"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING = "issue_addon_detached_addon_missing"
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
|
||||
|
||||
CORE_CONTAINER = "homeassistant"
|
||||
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME
|
||||
@@ -53,7 +53,9 @@ from .const import (
|
||||
SupervisorEntityModel,
|
||||
)
|
||||
from .handler import HassIO, HassioAPIError
|
||||
from .issues import SupervisorIssues
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .issues import SupervisorIssues
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -36,12 +36,17 @@ from .const import (
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_ADDON_URL,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
SupervisorIssueContext,
|
||||
)
|
||||
from .coordinator import get_addons_info
|
||||
from .handler import HassIO, HassioAPIError
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
@@ -93,6 +98,8 @@ ISSUE_KEYS_FOR_REPAIRS = {
|
||||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -258,6 +265,20 @@ class SupervisorIssues:
|
||||
placeholders: dict[str, str] | None = None
|
||||
if issue.reference:
|
||||
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
|
||||
|
||||
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
|
||||
addons = get_addons_info(self._hass)
|
||||
if addons and issue.reference in addons:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
|
||||
"name"
|
||||
]
|
||||
if "url" in addons[issue.reference]:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
|
||||
issue.reference
|
||||
]["url"]
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
|
@@ -14,7 +14,9 @@ from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from . import get_addons_info, get_issues_info
|
||||
from .const import (
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
PLACEHOLDER_KEY_ADDON,
|
||||
PLACEHOLDER_KEY_COMPONENTS,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
SupervisorIssueContext,
|
||||
@@ -22,12 +24,23 @@ from .const import (
|
||||
from .handler import async_apply_suggestion
|
||||
from .issues import Issue, Suggestion
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {"system_adopt_data_disk", "system_execute_reboot"}
|
||||
HELP_URLS = {
|
||||
"help_url": "https://www.home-assistant.io/help/",
|
||||
"community_url": "https://community.home-assistant.io/",
|
||||
}
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {
|
||||
"addon_execute_remove",
|
||||
"system_adopt_data_disk",
|
||||
"system_execute_reboot",
|
||||
}
|
||||
|
||||
|
||||
EXTRA_PLACEHOLDERS = {
|
||||
"issue_mount_mount_failed": {
|
||||
"storage_url": "/config/storage",
|
||||
}
|
||||
},
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED: HELP_URLS,
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +181,25 @@ class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
return placeholders
|
||||
|
||||
|
||||
class DetachedAddonIssueRepairFlow(SupervisorIssueRepairFlow):
|
||||
"""Handler for detached addon issue fixing flows."""
|
||||
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
"""Get description placeholders for steps."""
|
||||
placeholders: dict[str, str] = super().description_placeholders or {}
|
||||
if self.issue and self.issue.reference:
|
||||
addons = get_addons_info(self.hass)
|
||||
if addons and self.issue.reference in addons:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = addons[self.issue.reference][
|
||||
"name"
|
||||
]
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = self.issue.reference
|
||||
|
||||
return placeholders or None
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
@@ -178,5 +210,7 @@ async def async_create_fix_flow(
|
||||
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
|
||||
return DockerConfigIssueRepairFlow(issue_id)
|
||||
if issue and issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED:
|
||||
return DetachedAddonIssueRepairFlow(issue_id)
|
||||
|
||||
return SupervisorIssueRepairFlow(issue_id)
|
||||
|
@@ -17,6 +17,23 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_addon_detached_addon_missing": {
|
||||
"title": "Missing repository for an installed add-on",
|
||||
"description": "Repository for add-on {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nPlease check the [add-on's documentation]({addon_url}) for installation instructions and add the repository to the store."
|
||||
},
|
||||
"issue_addon_detached_addon_removed": {
|
||||
"title": "Installed add-on has been removed from repository",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"addon_execute_remove": {
|
||||
"description": "Add-on {addon} has been removed from the repository it was installed from. This means it will not get updates, and backups may not be restored correctly as the supervisor may not be able to build/download the resources required.\n\nClicking submit will uninstall this deprecated add-on. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not uninstall the add-on. Check the Supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_mount_mount_failed": {
|
||||
"title": "Network storage device failed",
|
||||
"fix_flow": {
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.48", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.49", "babel==2.13.1"]
|
||||
}
|
||||
|
@@ -20,15 +20,17 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "jewish_calendar"
|
||||
CONF_DIASPORA = "diaspora"
|
||||
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
|
||||
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
|
||||
DEFAULT_NAME = "Jewish Calendar"
|
||||
DEFAULT_CANDLE_LIGHT = 18
|
||||
DEFAULT_DIASPORA = False
|
||||
DEFAULT_HAVDALAH_OFFSET_MINUTES = 0
|
||||
DEFAULT_LANGUAGE = "english"
|
||||
from .const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_CANDLE_LIGHT,
|
||||
DEFAULT_DIASPORA,
|
||||
DEFAULT_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
@@ -22,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DEFAULT_NAME, DOMAIN
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
@@ -32,15 +32,17 @@ from homeassistant.helpers.selector import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "jewish_calendar"
|
||||
CONF_DIASPORA = "diaspora"
|
||||
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
|
||||
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
|
||||
DEFAULT_NAME = "Jewish Calendar"
|
||||
DEFAULT_CANDLE_LIGHT = 18
|
||||
DEFAULT_DIASPORA = False
|
||||
DEFAULT_HAVDALAH_OFFSET_MINUTES = 0
|
||||
DEFAULT_LANGUAGE = "english"
|
||||
from .const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_CANDLE_LIGHT,
|
||||
DEFAULT_DIASPORA,
|
||||
DEFAULT_HAVDALAH_OFFSET_MINUTES,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
LANGUAGE = [
|
||||
SelectOptionDict(value="hebrew", label="Hebrew"),
|
||||
|
13
homeassistant/components/jewish_calendar/const.py
Normal file
13
homeassistant/components/jewish_calendar/const.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Jewish Calendar constants."""
|
||||
|
||||
DOMAIN = "jewish_calendar"
|
||||
|
||||
CONF_DIASPORA = "diaspora"
|
||||
CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset"
|
||||
CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset"
|
||||
|
||||
DEFAULT_NAME = "Jewish Calendar"
|
||||
DEFAULT_CANDLE_LIGHT = 18
|
||||
DEFAULT_DIASPORA = False
|
||||
DEFAULT_HAVDALAH_OFFSET_MINUTES = 0
|
||||
DEFAULT_LANGUAGE = "english"
|
@@ -22,7 +22,7 @@ from homeassistant.helpers.sun import get_astral_event_date
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DEFAULT_NAME, DOMAIN
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@@ -14,7 +14,7 @@ from homeassistant import config as conf_util
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DISCOVERY, CONF_PAYLOAD, SERVICE_RELOAD
|
||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import (
|
||||
ConfigValidationError,
|
||||
ServiceValidationError,
|
||||
@@ -72,8 +72,7 @@ from .const import ( # noqa: F401
|
||||
DEFAULT_QOS,
|
||||
DEFAULT_RETAIN,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTED,
|
||||
MQTT_DISCONNECTED,
|
||||
MQTT_CONNECTION_STATE,
|
||||
RELOADABLE_PLATFORMS,
|
||||
TEMPLATE_ERRORS,
|
||||
)
|
||||
@@ -475,29 +474,9 @@ def async_subscribe_connection_status(
|
||||
hass: HomeAssistant, connection_status_callback: ConnectionStatusCallback
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to MQTT connection changes."""
|
||||
connection_status_callback_job = HassJob(connection_status_callback)
|
||||
|
||||
async def connected() -> None:
|
||||
task = hass.async_run_hass_job(connection_status_callback_job, True)
|
||||
if task:
|
||||
await task
|
||||
|
||||
async def disconnected() -> None:
|
||||
task = hass.async_run_hass_job(connection_status_callback_job, False)
|
||||
if task:
|
||||
await task
|
||||
|
||||
subscriptions = {
|
||||
"connect": async_dispatcher_connect(hass, MQTT_CONNECTED, connected),
|
||||
"disconnect": async_dispatcher_connect(hass, MQTT_DISCONNECTED, disconnected),
|
||||
}
|
||||
|
||||
@callback
|
||||
def unsubscribe() -> None:
|
||||
subscriptions["connect"]()
|
||||
subscriptions["disconnect"]()
|
||||
|
||||
return unsubscribe
|
||||
return async_dispatcher_connect(
|
||||
hass, MQTT_CONNECTION_STATE, connection_status_callback
|
||||
)
|
||||
|
||||
|
||||
def is_connected(hass: HomeAssistant) -> bool:
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -25,7 +24,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -35,8 +34,6 @@ from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_SUPPORTED_FEATURES,
|
||||
@@ -203,26 +200,11 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
|
||||
return
|
||||
self._attr_state = str(payload)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._config[CONF_STATE_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_state"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC, self._state_message_received, {"_attr_state"}
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -26,7 +25,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.helpers.event as evt
|
||||
@@ -37,7 +36,7 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RO_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .const import CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .mixins import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttValueTemplate, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
@@ -231,26 +230,11 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
|
||||
self.hass, off_delay, self._off_delay_listener
|
||||
)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._config[CONF_STATE_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_is_on", "_expired"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC, self._state_message_received, {"_attr_is_on", "_expired"}
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -8,7 +8,7 @@ from homeassistant.components import button
|
||||
from homeassistant.components.button import DEVICE_CLASSES_SCHEMA, ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -73,6 +73,7 @@ class MqttButton(MqttEntity, ButtonEntity):
|
||||
).async_render
|
||||
self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from base64 import b64decode
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -13,14 +12,14 @@ from homeassistant.components import camera
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_QOS, CONF_TOPIC
|
||||
from .const import CONF_TOPIC
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
@@ -107,26 +106,12 @@ class MqttCamera(MqttEntity, Camera):
|
||||
assert isinstance(msg.payload, bytes)
|
||||
self._last_image = msg.payload
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._config[CONF_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._image_received,
|
||||
None,
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
self.add_subscription(
|
||||
CONF_TOPIC, self._image_received, None, disable_encoding=True
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -69,8 +69,7 @@ from .const import (
|
||||
DEFAULT_WS_HEADERS,
|
||||
DEFAULT_WS_PATH,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTED,
|
||||
MQTT_DISCONNECTED,
|
||||
MQTT_CONNECTION_STATE,
|
||||
PROTOCOL_5,
|
||||
PROTOCOL_31,
|
||||
TRANSPORT_WEBSOCKETS,
|
||||
@@ -1033,7 +1032,7 @@ class MQTT:
|
||||
return
|
||||
|
||||
self.connected = True
|
||||
async_dispatcher_send(self.hass, MQTT_CONNECTED)
|
||||
async_dispatcher_send(self.hass, MQTT_CONNECTION_STATE, True)
|
||||
_LOGGER.debug(
|
||||
"Connected to MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
@@ -1229,7 +1228,7 @@ class MQTT:
|
||||
# result is set make sure the first connection result is set
|
||||
self._async_connection_result(False)
|
||||
self.connected = False
|
||||
async_dispatcher_send(self.hass, MQTT_DISCONNECTED)
|
||||
async_dispatcher_send(self.hass, MQTT_CONNECTION_STATE, False)
|
||||
_LOGGER.warning(
|
||||
"Disconnected from MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
|
@@ -43,7 +43,7 @@ from homeassistant.const import (
|
||||
PRECISION_WHOLE,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -59,7 +59,6 @@ from .const import (
|
||||
CONF_CURRENT_HUMIDITY_TOPIC,
|
||||
CONF_CURRENT_TEMP_TEMPLATE,
|
||||
CONF_CURRENT_TEMP_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_MODE_COMMAND_TEMPLATE,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_MODE_LIST,
|
||||
@@ -68,7 +67,6 @@ from .const import (
|
||||
CONF_POWER_COMMAND_TEMPLATE,
|
||||
CONF_POWER_COMMAND_TOPIC,
|
||||
CONF_PRECISION,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_TEMP_COMMAND_TEMPLATE,
|
||||
CONF_TEMP_COMMAND_TOPIC,
|
||||
@@ -409,29 +407,6 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
_command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]]
|
||||
_value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]]
|
||||
|
||||
def add_subscription(
|
||||
self,
|
||||
topics: dict[str, dict[str, Any]],
|
||||
topic: str,
|
||||
msg_callback: Callable[[ReceiveMessage], None],
|
||||
tracked_attributes: set[str],
|
||||
) -> None:
|
||||
"""Add a subscription."""
|
||||
qos: int = self._config[CONF_QOS]
|
||||
if topic in self._topic and self._topic[topic] is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._topic[topic],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
msg_callback,
|
||||
tracked_attributes,
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": qos,
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
def render_template(
|
||||
self, msg: ReceiveMessage, template_name: str
|
||||
) -> ReceivePayloadType:
|
||||
@@ -462,11 +437,9 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
@callback
|
||||
def prepare_subscribe_topics(
|
||||
self,
|
||||
topics: dict[str, dict[str, Any]],
|
||||
) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_CURRENT_TEMP_TOPIC,
|
||||
partial(
|
||||
self.handle_climate_attribute_received,
|
||||
@@ -476,7 +449,6 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
{"_attr_current_temperature"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_TEMP_STATE_TOPIC,
|
||||
partial(
|
||||
self.handle_climate_attribute_received,
|
||||
@@ -486,7 +458,6 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
{"_attr_target_temperature"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_TEMP_LOW_STATE_TOPIC,
|
||||
partial(
|
||||
self.handle_climate_attribute_received,
|
||||
@@ -496,7 +467,6 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
{"_attr_target_temperature_low"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_TEMP_HIGH_STATE_TOPIC,
|
||||
partial(
|
||||
self.handle_climate_attribute_received,
|
||||
@@ -506,10 +476,6 @@ class MqttTemperatureControlEntity(MqttEntity, ABC):
|
||||
{"_attr_target_temperature_high"},
|
||||
)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
@@ -761,16 +727,13 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# add subscriptions for MqttClimate
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_ACTION_TOPIC,
|
||||
self._handle_action_received,
|
||||
{"_attr_hvac_action"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_CURRENT_HUMIDITY_TOPIC,
|
||||
partial(
|
||||
self.handle_climate_attribute_received,
|
||||
@@ -780,7 +743,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||
{"_attr_current_humidity"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_HUMIDITY_STATE_TOPIC,
|
||||
partial(
|
||||
self.handle_climate_attribute_received,
|
||||
@@ -790,7 +752,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||
{"_attr_target_humidity"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
partial(
|
||||
self._handle_mode_received,
|
||||
@@ -801,7 +762,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||
{"_attr_hvac_mode"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
partial(
|
||||
self._handle_mode_received,
|
||||
@@ -812,7 +772,6 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||
{"_attr_fan_mode"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_SWING_MODE_STATE_TOPIC,
|
||||
partial(
|
||||
self._handle_mode_received,
|
||||
@@ -823,13 +782,12 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||
{"_attr_swing_mode"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_PRESET_MODE_STATE_TOPIC,
|
||||
self._handle_preset_mode_received,
|
||||
{"_attr_preset_mode"},
|
||||
)
|
||||
|
||||
self.prepare_subscribe_topics(topics)
|
||||
# add subscriptions for MqttTemperatureControlEntity
|
||||
self.prepare_subscribe_topics()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperatures."""
|
||||
|
@@ -150,8 +150,7 @@ DEFAULT_WILL = {
|
||||
|
||||
DOMAIN = "mqtt"
|
||||
|
||||
MQTT_CONNECTED = "mqtt_connected"
|
||||
MQTT_DISCONNECTED = "mqtt_disconnected"
|
||||
MQTT_CONNECTION_STATE = "mqtt_connection_state"
|
||||
|
||||
PAYLOAD_EMPTY_JSON = "{}"
|
||||
PAYLOAD_NONE = "None"
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -28,7 +27,7 @@ from homeassistant.const import (
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||
@@ -43,13 +42,11 @@ from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD_CLOSE,
|
||||
CONF_PAYLOAD_OPEN,
|
||||
CONF_PAYLOAD_STOP,
|
||||
CONF_POSITION_CLOSED,
|
||||
CONF_POSITION_OPEN,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_CLOSED,
|
||||
CONF_STATE_CLOSING,
|
||||
@@ -457,60 +454,29 @@ class MqttCover(MqttEntity, CoverEntity):
|
||||
STATE_CLOSED if self.current_cover_position == 0 else STATE_OPEN
|
||||
)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
|
||||
if self._config.get(CONF_GET_POSITION_TOPIC):
|
||||
topics["get_position_topic"] = {
|
||||
"topic": self._config.get(CONF_GET_POSITION_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._position_message_received,
|
||||
{
|
||||
"_attr_current_cover_position",
|
||||
"_attr_current_cover_tilt_position",
|
||||
"_attr_is_closed",
|
||||
"_attr_is_closing",
|
||||
"_attr_is_opening",
|
||||
},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
if self._config.get(CONF_STATE_TOPIC):
|
||||
topics["state_topic"] = {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_is_closed", "_attr_is_closing", "_attr_is_opening"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
if self._config.get(CONF_TILT_STATUS_TOPIC) is not None:
|
||||
topics["tilt_status_topic"] = {
|
||||
"topic": self._config.get(CONF_TILT_STATUS_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._tilt_message_received,
|
||||
{"_attr_current_cover_tilt_position"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
self.add_subscription(
|
||||
CONF_GET_POSITION_TOPIC,
|
||||
self._position_message_received,
|
||||
{
|
||||
"_attr_current_cover_position",
|
||||
"_attr_current_cover_tilt_position",
|
||||
"_attr_is_closed",
|
||||
"_attr_is_closing",
|
||||
"_attr_is_opening",
|
||||
},
|
||||
)
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_message_received,
|
||||
{"_attr_is_closed", "_attr_is_closing", "_attr_is_opening"},
|
||||
)
|
||||
self.add_subscription(
|
||||
CONF_TILT_STATUS_TOPIC,
|
||||
self._tilt_message_received,
|
||||
{"_attr_current_cover_tilt_position"},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -25,14 +24,14 @@ from homeassistant.const import (
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_PAYLOAD_RESET, CONF_QOS, CONF_STATE_TOPIC
|
||||
from .const import CONF_PAYLOAD_RESET, CONF_STATE_TOPIC
|
||||
from .mixins import CONF_JSON_ATTRS_TOPIC, MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
@@ -136,28 +135,11 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||
assert isinstance(msg.payload, str)
|
||||
self._location_name = msg.payload
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
state_topic: str | None = self._config.get(CONF_STATE_TOPIC)
|
||||
if state_topic is None:
|
||||
return
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": state_topic,
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._tracker_message_received,
|
||||
{"_location_name"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC, self._tracker_message_received, {"_location_name"}
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -17,7 +16,7 @@ from homeassistant.components.event import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -25,13 +24,7 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads_object
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RO_SCHEMA
|
||||
from .const import (
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_EMPTY_JSON,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from .const import CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON, PAYLOAD_NONE
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
DATA_MQTT,
|
||||
@@ -186,26 +179,10 @@ class MqttEvent(MqttEntity, EventEntity):
|
||||
mqtt_data = self.hass.data[DATA_MQTT]
|
||||
mqtt_data.state_write_requests.write_state_request(self)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, dict[str, Any]] = {}
|
||||
|
||||
topics["state_topic"] = {
|
||||
"topic": self._config[CONF_STATE_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._event_received,
|
||||
None,
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
)
|
||||
self.add_subscription(CONF_STATE_TOPIC, self._event_received, None)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
@@ -27,7 +26,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -43,15 +42,12 @@ from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
MessageCallbackType,
|
||||
MqttCommandTemplate,
|
||||
MqttValueTemplate,
|
||||
PublishPayloadType,
|
||||
@@ -429,52 +425,30 @@ class MqttFan(MqttEntity, FanEntity):
|
||||
return
|
||||
self._attr_current_direction = str(direction)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, Any] = {}
|
||||
|
||||
def add_subscribe_topic(
|
||||
topic: str, msg_callback: MessageCallbackType, tracked_attributes: set[str]
|
||||
) -> bool:
|
||||
"""Add a topic to subscribe to."""
|
||||
if has_topic := self._topic[topic] is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._topic[topic],
|
||||
"msg_callback": partial(
|
||||
self._message_callback, msg_callback, tracked_attributes
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
return has_topic
|
||||
|
||||
add_subscribe_topic(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"})
|
||||
add_subscribe_topic(
|
||||
self.add_subscription(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"})
|
||||
self.add_subscription(
|
||||
CONF_PERCENTAGE_STATE_TOPIC, self._percentage_received, {"_attr_percentage"}
|
||||
)
|
||||
add_subscribe_topic(
|
||||
self.add_subscription(
|
||||
CONF_PRESET_MODE_STATE_TOPIC,
|
||||
self._preset_mode_received,
|
||||
{"_attr_preset_mode"},
|
||||
)
|
||||
if add_subscribe_topic(
|
||||
if self.add_subscription(
|
||||
CONF_OSCILLATION_STATE_TOPIC,
|
||||
self._oscillation_received,
|
||||
{"_attr_oscillating"},
|
||||
):
|
||||
self._attr_oscillating = False
|
||||
add_subscribe_topic(
|
||||
self.add_subscription(
|
||||
CONF_DIRECTION_STATE_TOPIC,
|
||||
self._direction_received,
|
||||
{"_attr_current_direction"},
|
||||
)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -30,7 +29,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_STATE,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -45,8 +44,6 @@ from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_CURRENT_HUMIDITY_TEMPLATE,
|
||||
CONF_CURRENT_HUMIDITY_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_NONE,
|
||||
@@ -274,27 +271,6 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
for key, tpl in value_templates.items()
|
||||
}
|
||||
|
||||
def add_subscription(
|
||||
self,
|
||||
topics: dict[str, dict[str, Any]],
|
||||
topic: str,
|
||||
msg_callback: Callable[[ReceiveMessage], None],
|
||||
tracked_attributes: set[str],
|
||||
) -> None:
|
||||
"""Add a subscription."""
|
||||
qos: int = self._config[CONF_QOS]
|
||||
if topic in self._topic and self._topic[topic] is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._topic[topic],
|
||||
"msg_callback": partial(
|
||||
self._message_callback, msg_callback, tracked_attributes
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": qos,
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
@callback
|
||||
def _state_received(self, msg: ReceiveMessage) -> None:
|
||||
"""Handle new received MQTT message."""
|
||||
@@ -415,34 +391,25 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||
|
||||
self._attr_mode = mode
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, Any] = {}
|
||||
|
||||
self.add_subscription(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"})
|
||||
self.add_subscription(
|
||||
topics, CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"}
|
||||
CONF_ACTION_TOPIC, self._action_received, {"_attr_action"}
|
||||
)
|
||||
self.add_subscription(
|
||||
topics, CONF_ACTION_TOPIC, self._action_received, {"_attr_action"}
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_CURRENT_HUMIDITY_TOPIC,
|
||||
self._current_humidity_received,
|
||||
{"_attr_current_humidity"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_TARGET_HUMIDITY_STATE_TOPIC,
|
||||
self._target_humidity_received,
|
||||
{"_attr_target_humidity"},
|
||||
)
|
||||
self.add_subscription(
|
||||
topics, CONF_MODE_STATE_TOPIC, self._mode_received, {"_attr_mode"}
|
||||
)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
CONF_MODE_STATE_TOPIC, self._mode_received, {"_attr_mode"}
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from base64 import b64decode
|
||||
import binascii
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -16,7 +15,7 @@ from homeassistant.components import image
|
||||
from homeassistant.components.image import DEFAULT_CONTENT_TYPE, ImageEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
@@ -26,11 +25,9 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
DATA_MQTT,
|
||||
MessageCallbackType,
|
||||
MqttValueTemplate,
|
||||
MqttValueTemplateException,
|
||||
ReceiveMessage,
|
||||
@@ -182,35 +179,14 @@ class MqttImage(MqttEntity, ImageEntity):
|
||||
self._cached_image = None
|
||||
self.hass.data[DATA_MQTT].state_write_requests.write_state_request(self)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
topics: dict[str, Any] = {}
|
||||
|
||||
def add_subscribe_topic(topic: str, msg_callback: MessageCallbackType) -> bool:
|
||||
"""Add a topic to subscribe to."""
|
||||
encoding: str | None
|
||||
encoding = (
|
||||
None
|
||||
if CONF_IMAGE_TOPIC in self._config
|
||||
else self._config[CONF_ENCODING] or None
|
||||
)
|
||||
if has_topic := self._topic[topic] is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._topic[topic],
|
||||
"msg_callback": partial(self._message_callback, msg_callback, None),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": encoding,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
return has_topic
|
||||
|
||||
add_subscribe_topic(CONF_IMAGE_TOPIC, self._image_data_received)
|
||||
add_subscribe_topic(CONF_URL_TOPIC, self._image_from_url_request_received)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
self.add_subscription(
|
||||
CONF_IMAGE_TOPIC, self._image_data_received, None, disable_encoding=True
|
||||
)
|
||||
self.add_subscription(
|
||||
CONF_URL_TOPIC, self._image_from_url_request_received, None
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import contextlib
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -17,7 +16,7 @@ from homeassistant.components.lawn_mower import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -25,13 +24,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
DEFAULT_OPTIMISTIC,
|
||||
DEFAULT_RETAIN,
|
||||
)
|
||||
from .const import CONF_RETAIN, DEFAULT_OPTIMISTIC, DEFAULT_RETAIN
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
MqttCommandTemplate,
|
||||
@@ -172,30 +165,15 @@ class MqttLawnMower(MqttEntity, LawnMowerEntity, RestoreEntity):
|
||||
)
|
||||
return
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
if self._config.get(CONF_ACTIVITY_STATE_TOPIC) is None:
|
||||
if not self.add_subscription(
|
||||
CONF_ACTIVITY_STATE_TOPIC, self._message_received, {"_attr_activity"}
|
||||
):
|
||||
# Force into optimistic mode.
|
||||
self._attr_assumed_state = True
|
||||
return
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
CONF_ACTIVITY_STATE_TOPIC: {
|
||||
"topic": self._config.get(CONF_ACTIVITY_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._message_received,
|
||||
{"_attr_activity"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -37,7 +36,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HassJobType, callback
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -47,15 +46,12 @@ from .. import subscription
|
||||
from ..config import MQTT_RW_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from ..mixins import MqttEntity
|
||||
from ..models import (
|
||||
MessageCallbackType,
|
||||
MqttCommandTemplate,
|
||||
MqttValueTemplate,
|
||||
PayloadSentinel,
|
||||
@@ -562,69 +558,50 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity):
|
||||
self._attr_color_mode = ColorMode.XY
|
||||
self._attr_xy_color = cast(tuple[float, float], xy_color)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None: # noqa: C901
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, dict[str, Any]] = {}
|
||||
|
||||
def add_topic(
|
||||
topic: str, msg_callback: MessageCallbackType, tracked_attributes: set[str]
|
||||
) -> None:
|
||||
"""Add a topic."""
|
||||
if self._topic[topic] is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._topic[topic],
|
||||
"msg_callback": partial(
|
||||
self._message_callback, msg_callback, tracked_attributes
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
add_topic(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"})
|
||||
add_topic(
|
||||
self.add_subscription(CONF_STATE_TOPIC, self._state_received, {"_attr_is_on"})
|
||||
self.add_subscription(
|
||||
CONF_BRIGHTNESS_STATE_TOPIC, self._brightness_received, {"_attr_brightness"}
|
||||
)
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_RGB_STATE_TOPIC,
|
||||
self._rgb_received,
|
||||
{"_attr_brightness", "_attr_color_mode", "_attr_rgb_color"},
|
||||
)
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_RGBW_STATE_TOPIC,
|
||||
self._rgbw_received,
|
||||
{"_attr_brightness", "_attr_color_mode", "_attr_rgbw_color"},
|
||||
)
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_RGBWW_STATE_TOPIC,
|
||||
self._rgbww_received,
|
||||
{"_attr_brightness", "_attr_color_mode", "_attr_rgbww_color"},
|
||||
)
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_COLOR_MODE_STATE_TOPIC, self._color_mode_received, {"_attr_color_mode"}
|
||||
)
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_COLOR_TEMP_STATE_TOPIC,
|
||||
self._color_temp_received,
|
||||
{"_attr_color_mode", "_attr_color_temp"},
|
||||
)
|
||||
add_topic(CONF_EFFECT_STATE_TOPIC, self._effect_received, {"_attr_effect"})
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_EFFECT_STATE_TOPIC, self._effect_received, {"_attr_effect"}
|
||||
)
|
||||
self.add_subscription(
|
||||
CONF_HS_STATE_TOPIC,
|
||||
self._hs_received,
|
||||
{"_attr_color_mode", "_attr_hs_color"},
|
||||
)
|
||||
add_topic(
|
||||
self.add_subscription(
|
||||
CONF_XY_STATE_TOPIC,
|
||||
self._xy_received,
|
||||
{"_attr_color_mode", "_attr_xy_color"},
|
||||
)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
@@ -47,7 +46,7 @@ from homeassistant.const import (
|
||||
CONF_XY,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HassJobType, async_get_hass, callback
|
||||
from homeassistant.core import async_get_hass, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
@@ -61,7 +60,6 @@ from .. import subscription
|
||||
from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -490,40 +488,23 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
with suppress(KeyError):
|
||||
self._attr_effect = cast(str, values["effect"])
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
#
|
||||
if self._topic[CONF_STATE_TOPIC] is None:
|
||||
return
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_received,
|
||||
{
|
||||
CONF_STATE_TOPIC: {
|
||||
"topic": self._topic[CONF_STATE_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_received,
|
||||
{
|
||||
"_attr_brightness",
|
||||
"_attr_color_temp",
|
||||
"_attr_effect",
|
||||
"_attr_hs_color",
|
||||
"_attr_is_on",
|
||||
"_attr_rgb_color",
|
||||
"_attr_rgbw_color",
|
||||
"_attr_rgbww_color",
|
||||
"_attr_xy_color",
|
||||
"color_mode",
|
||||
},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
"_attr_brightness",
|
||||
"_attr_color_temp",
|
||||
"_attr_effect",
|
||||
"_attr_hs_color",
|
||||
"_attr_is_on",
|
||||
"_attr_rgb_color",
|
||||
"_attr_rgbw_color",
|
||||
"_attr_rgbww_color",
|
||||
"_attr_xy_color",
|
||||
"color_mode",
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -29,7 +28,7 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HassJobType, callback
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
@@ -37,13 +36,7 @@ import homeassistant.util.color as color_util
|
||||
|
||||
from .. import subscription
|
||||
from ..config import MQTT_RW_SCHEMA
|
||||
from ..const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from ..const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from ..mixins import MqttEntity
|
||||
from ..models import (
|
||||
MqttCommandTemplate,
|
||||
@@ -254,35 +247,19 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
else:
|
||||
_LOGGER.warning("Unsupported effect value received")
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
if self._topics[CONF_STATE_TOPIC] is None:
|
||||
return
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_received,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._topics[CONF_STATE_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_received,
|
||||
{
|
||||
"_attr_brightness",
|
||||
"_attr_color_mode",
|
||||
"_attr_color_temp",
|
||||
"_attr_effect",
|
||||
"_attr_hs_color",
|
||||
"_attr_is_on",
|
||||
},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
"_attr_brightness",
|
||||
"_attr_color_mode",
|
||||
"_attr_color_temp",
|
||||
"_attr_effect",
|
||||
"_attr_hs_color",
|
||||
"_attr_is_on",
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
@@ -19,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_OPTIMISTIC,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
@@ -29,9 +28,7 @@ from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD_RESET,
|
||||
CONF_QOS,
|
||||
CONF_STATE_OPEN,
|
||||
CONF_STATE_OPENING,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -203,42 +200,20 @@ class MqttLock(MqttEntity, LockEntity):
|
||||
self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING]
|
||||
self._attr_is_jammed = payload == self._config[CONF_STATE_JAMMED]
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, dict[str, Any]]
|
||||
qos: int = self._config[CONF_QOS]
|
||||
encoding: str | None = self._config[CONF_ENCODING] or None
|
||||
|
||||
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
return
|
||||
topics = {
|
||||
CONF_STATE_TOPIC: {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._message_received,
|
||||
{
|
||||
"_attr_is_jammed",
|
||||
"_attr_is_locked",
|
||||
"_attr_is_locking",
|
||||
"_attr_is_open",
|
||||
"_attr_is_opening",
|
||||
"_attr_is_unlocking",
|
||||
},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
CONF_QOS: qos,
|
||||
CONF_ENCODING: encoding,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
topics,
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._message_received,
|
||||
{
|
||||
"_attr_is_jammed",
|
||||
"_attr_is_locked",
|
||||
"_attr_is_locking",
|
||||
"_attr_is_open",
|
||||
"_attr_is_opening",
|
||||
"_attr_is_unlocking",
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -92,8 +92,7 @@ from .const import (
|
||||
CONF_VIA_DEVICE,
|
||||
DEFAULT_ENCODING,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTED,
|
||||
MQTT_DISCONNECTED,
|
||||
MQTT_CONNECTION_STATE,
|
||||
)
|
||||
from .debug_info import log_message
|
||||
from .discovery import (
|
||||
@@ -460,12 +459,11 @@ class MqttAvailabilityMixin(Entity):
|
||||
await super().async_added_to_hass()
|
||||
self._availability_prepare_subscribe_topics()
|
||||
self._availability_subscribe_topics()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect
|
||||
self.hass,
|
||||
MQTT_CONNECTION_STATE,
|
||||
self.async_mqtt_connection_state_changed,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -498,10 +496,10 @@ class MqttAvailabilityMixin(Entity):
|
||||
}
|
||||
|
||||
for avail_topic_conf in self._avail_topics.values():
|
||||
avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = MqttValueTemplate(
|
||||
avail_topic_conf[CONF_AVAILABILITY_TEMPLATE],
|
||||
entity=self,
|
||||
).async_render_with_possible_json_value
|
||||
if template := avail_topic_conf[CONF_AVAILABILITY_TEMPLATE]:
|
||||
avail_topic_conf[CONF_AVAILABILITY_TEMPLATE] = MqttValueTemplate(
|
||||
template, entity=self
|
||||
).async_render_with_possible_json_value
|
||||
|
||||
self._avail_config = config
|
||||
|
||||
@@ -537,7 +535,9 @@ class MqttAvailabilityMixin(Entity):
|
||||
"""Handle a new received MQTT availability message."""
|
||||
topic = msg.topic
|
||||
avail_topic = self._avail_topics[topic]
|
||||
payload = avail_topic[CONF_AVAILABILITY_TEMPLATE](msg.payload)
|
||||
template = avail_topic[CONF_AVAILABILITY_TEMPLATE]
|
||||
payload = template(msg.payload) if template else msg.payload
|
||||
|
||||
if payload == avail_topic[CONF_PAYLOAD_AVAILABLE]:
|
||||
self._available[topic] = True
|
||||
self._available_latest = True
|
||||
@@ -551,7 +551,7 @@ class MqttAvailabilityMixin(Entity):
|
||||
async_subscribe_topics_internal(self.hass, self._availability_sub_state)
|
||||
|
||||
@callback
|
||||
def async_mqtt_connect(self) -> None:
|
||||
def async_mqtt_connection_state_changed(self, state: bool) -> None:
|
||||
"""Update state on connection/disconnection to MQTT broker."""
|
||||
if not self.hass.is_stopping:
|
||||
self.async_write_ha_state()
|
||||
@@ -1069,6 +1069,7 @@ class MqttEntity(
|
||||
self._attr_unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._sub_state: dict[str, EntitySubscription] = {}
|
||||
self._discovery = discovery_data is not None
|
||||
self._subscriptions: dict[str, dict[str, Any]]
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(self._config)
|
||||
@@ -1095,7 +1096,14 @@ class MqttEntity(
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to MQTT events."""
|
||||
await super().async_added_to_hass()
|
||||
self._subscriptions = {}
|
||||
self._prepare_subscribe_topics()
|
||||
if self._subscriptions:
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
self._subscriptions,
|
||||
)
|
||||
await self._subscribe_topics()
|
||||
await self.mqtt_async_added_to_hass()
|
||||
|
||||
@@ -1120,7 +1128,14 @@ class MqttEntity(
|
||||
self.attributes_prepare_discovery_update(config)
|
||||
self.availability_prepare_discovery_update(config)
|
||||
self.device_info_discovery_update(config)
|
||||
self._subscriptions = {}
|
||||
self._prepare_subscribe_topics()
|
||||
if self._subscriptions:
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
self._subscriptions,
|
||||
)
|
||||
|
||||
# Finalize MQTT subscriptions
|
||||
await self.attributes_discovery_update(config)
|
||||
@@ -1210,6 +1225,7 @@ class MqttEntity(
|
||||
"""(Re)Setup the entity."""
|
||||
|
||||
@abstractmethod
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
@@ -1258,6 +1274,35 @@ class MqttEntity(
|
||||
if attributes is not None and self._attrs_have_changed(attrs_snapshot):
|
||||
mqtt_data.state_write_requests.write_state_request(self)
|
||||
|
||||
def add_subscription(
|
||||
self,
|
||||
state_topic_config_key: str,
|
||||
msg_callback: Callable[[ReceiveMessage], None],
|
||||
tracked_attributes: set[str] | None,
|
||||
disable_encoding: bool = False,
|
||||
) -> bool:
|
||||
"""Add a subscription."""
|
||||
qos: int = self._config[CONF_QOS]
|
||||
encoding: str | None = None
|
||||
if not disable_encoding:
|
||||
encoding = self._config[CONF_ENCODING] or None
|
||||
if (
|
||||
state_topic_config_key in self._config
|
||||
and self._config[state_topic_config_key] is not None
|
||||
):
|
||||
self._subscriptions[state_topic_config_key] = {
|
||||
"topic": self._config[state_topic_config_key],
|
||||
"msg_callback": partial(
|
||||
self._message_callback, msg_callback, tracked_attributes
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": qos,
|
||||
"encoding": encoding,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def update_device(
|
||||
hass: HomeAssistant,
|
||||
|
@@ -8,7 +8,7 @@ from homeassistant.components import notify
|
||||
from homeassistant.components.notify import NotifyEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -68,6 +68,7 @@ class MqttNotify(MqttEntity, NotifyEntity):
|
||||
config.get(CONF_COMMAND_TEMPLATE), entity=self
|
||||
).async_render
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -26,7 +25,7 @@ from homeassistant.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -36,9 +35,7 @@ from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD_RESET,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
)
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
@@ -193,30 +190,15 @@ class MqttNumber(MqttEntity, RestoreNumber):
|
||||
|
||||
self._attr_native_value = num_value
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||
if not self.add_subscription(
|
||||
CONF_STATE_TOPIC, self._message_received, {"_attr_native_value"}
|
||||
):
|
||||
# Force into optimistic mode.
|
||||
self._attr_assumed_state = True
|
||||
return
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._message_received,
|
||||
{"_attr_native_value"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -10,7 +10,7 @@ from homeassistant.components import scene
|
||||
from homeassistant.components.scene import Scene
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_PAYLOAD_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -72,6 +72,7 @@ class MqttScene(
|
||||
def _setup_from_config(self, config: ConfigType) -> None:
|
||||
"""(Re)Setup the entity."""
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -12,7 +11,7 @@ from homeassistant.components import select
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -20,13 +19,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
)
|
||||
from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_STATE_TOPIC
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
MqttCommandTemplate,
|
||||
@@ -133,30 +126,15 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||
return
|
||||
self._attr_current_option = payload
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||
if not self.add_subscription(
|
||||
CONF_STATE_TOPIC, self._message_received, {"_attr_current_option"}
|
||||
):
|
||||
# Force into optimistic mode.
|
||||
self._attr_assumed_state = True
|
||||
return
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
"state_topic": {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._message_received,
|
||||
{"_attr_current_option"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -4,9 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -31,13 +29,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
HassJobType,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
@@ -46,7 +38,7 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RO_SCHEMA
|
||||
from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .const import CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .mixins import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
MqttValueTemplate,
|
||||
@@ -289,25 +281,13 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
if CONF_LAST_RESET_VALUE_TEMPLATE in self._config:
|
||||
_update_last_reset(msg)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, dict[str, Any]] = {}
|
||||
|
||||
topics["state_topic"] = {
|
||||
"topic": self._config[CONF_STATE_TOPIC],
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_native_value", "_attr_last_reset", "_expired"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_message_received,
|
||||
{"_attr_native_value", "_attr_last_reset", "_expired"},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -28,7 +27,7 @@ from homeassistant.const import (
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
@@ -41,8 +40,6 @@ from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_STATE_VALUE_TEMPLATE,
|
||||
PAYLOAD_EMPTY_JSON,
|
||||
@@ -261,30 +258,17 @@ class MqttSiren(MqttEntity, SirenEntity):
|
||||
self._extra_attributes = dict(self._extra_attributes)
|
||||
self._update(process_turn_on_params(self, params))
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||
if not self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_message_received,
|
||||
{"_attr_is_on", "_extra_attributes"},
|
||||
):
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
return
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
CONF_STATE_TOPIC: {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_is_on", "_extra_attributes"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -20,7 +19,7 @@ from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -29,13 +28,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from .const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC, PAYLOAD_NONE
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttValueTemplate, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
@@ -124,30 +117,15 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity):
|
||||
elif payload == PAYLOAD_NONE:
|
||||
self._attr_is_on = None
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
if self._config.get(CONF_STATE_TOPIC) is None:
|
||||
if not self.add_subscription(
|
||||
CONF_STATE_TOPIC, self._state_message_received, {"_attr_is_on"}
|
||||
):
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
return
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass,
|
||||
self._sub_state,
|
||||
{
|
||||
CONF_STATE_TOPIC: {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_is_on"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
@@ -20,23 +19,16 @@ from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
MAX_LENGTH_STATE_STATE,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import subscription
|
||||
from .config import MQTT_RW_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_STATE_TOPIC,
|
||||
)
|
||||
from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_STATE_TOPIC
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import (
|
||||
MessageCallbackType,
|
||||
MqttCommandTemplate,
|
||||
MqttValueTemplate,
|
||||
PublishPayloadType,
|
||||
@@ -163,39 +155,15 @@ class MqttTextEntity(MqttEntity, TextEntity):
|
||||
return
|
||||
self._attr_native_value = payload
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, Any] = {}
|
||||
|
||||
def add_subscription(
|
||||
topics: dict[str, Any],
|
||||
topic: str,
|
||||
msg_callback: MessageCallbackType,
|
||||
tracked_attributes: set[str],
|
||||
) -> None:
|
||||
if self._config.get(topic) is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._config[topic],
|
||||
"msg_callback": partial(
|
||||
self._message_callback, msg_callback, tracked_attributes
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
add_subscription(
|
||||
topics,
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._handle_state_message_received,
|
||||
{"_attr_native_value"},
|
||||
)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, TypedDict, cast
|
||||
|
||||
@@ -16,7 +15,7 @@ from homeassistant.components.update import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLATE
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -25,16 +24,9 @@ from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||
|
||||
from . import subscription
|
||||
from .config import DEFAULT_RETAIN, MQTT_RO_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_EMPTY_JSON,
|
||||
)
|
||||
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, PAYLOAD_EMPTY_JSON
|
||||
from .mixins import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MessageCallbackType, MqttValueTemplate, ReceiveMessage
|
||||
from .models import MqttValueTemplate, ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import valid_publish_topic, valid_subscribe_topic
|
||||
|
||||
@@ -210,30 +202,10 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
if isinstance(latest_version, str) and latest_version != "":
|
||||
self._attr_latest_version = latest_version
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, Any] = {}
|
||||
|
||||
def add_subscription(
|
||||
topics: dict[str, Any],
|
||||
topic: str,
|
||||
msg_callback: MessageCallbackType,
|
||||
tracked_attributes: set[str],
|
||||
) -> None:
|
||||
if self._config.get(topic) is not None:
|
||||
topics[topic] = {
|
||||
"topic": self._config[topic],
|
||||
"msg_callback": partial(
|
||||
self._message_callback, msg_callback, tracked_attributes
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
add_subscription(
|
||||
topics,
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._handle_state_message_received,
|
||||
{
|
||||
@@ -245,17 +217,12 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
"_entity_picture",
|
||||
},
|
||||
)
|
||||
add_subscription(
|
||||
topics,
|
||||
self.add_subscription(
|
||||
CONF_LATEST_VERSION_TOPIC,
|
||||
self._handle_latest_version_received,
|
||||
{"_attr_latest_version"},
|
||||
)
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
|
@@ -114,8 +114,6 @@ async def async_wait_for_mqtt_client(hass: HomeAssistant) -> bool:
|
||||
hass.data[DATA_MQTT_AVAILABLE] = state_reached_future
|
||||
else:
|
||||
state_reached_future = hass.data[DATA_MQTT_AVAILABLE]
|
||||
if state_reached_future.done():
|
||||
return state_reached_future.result()
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(AVAILABILITY_TIMEOUT):
|
||||
|
@@ -8,7 +8,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -31,7 +30,7 @@ from homeassistant.const import (
|
||||
STATE_IDLE,
|
||||
STATE_PAUSED,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, async_get_hass, callback
|
||||
from homeassistant.core import HomeAssistant, async_get_hass, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
@@ -43,8 +42,6 @@ from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_SCHEMA,
|
||||
CONF_STATE_TOPIC,
|
||||
@@ -331,25 +328,13 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
del payload[STATE]
|
||||
self._update_state_attributes(payload)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, Any] = {}
|
||||
|
||||
if state_topic := self._config.get(CONF_STATE_TOPIC):
|
||||
topics["state_position_topic"] = {
|
||||
"topic": state_topic,
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{"_attr_battery_level", "_attr_fan_speed", "_attr_state"},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_message_received,
|
||||
{"_attr_battery_level", "_attr_fan_speed", "_attr_state"},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -26,7 +25,7 @@ from homeassistant.const import (
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
)
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -41,13 +40,11 @@ from .config import MQTT_BASE_SCHEMA
|
||||
from .const import (
|
||||
CONF_COMMAND_TEMPLATE,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_ENCODING,
|
||||
CONF_PAYLOAD_CLOSE,
|
||||
CONF_PAYLOAD_OPEN,
|
||||
CONF_PAYLOAD_STOP,
|
||||
CONF_POSITION_CLOSED,
|
||||
CONF_POSITION_OPEN,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_CLOSED,
|
||||
CONF_STATE_CLOSING,
|
||||
@@ -337,31 +334,18 @@ class MqttValve(MqttEntity, ValveEntity):
|
||||
else:
|
||||
self._process_binary_valve_update(msg, state_payload)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
|
||||
if self._config.get(CONF_STATE_TOPIC):
|
||||
topics["state_topic"] = {
|
||||
"topic": self._config.get(CONF_STATE_TOPIC),
|
||||
"msg_callback": partial(
|
||||
self._message_callback,
|
||||
self._state_message_received,
|
||||
{
|
||||
"_attr_current_valve_position",
|
||||
"_attr_is_closed",
|
||||
"_attr_is_closing",
|
||||
"_attr_is_opening",
|
||||
},
|
||||
),
|
||||
"entity_id": self.entity_id,
|
||||
"qos": self._config[CONF_QOS],
|
||||
"encoding": self._config[CONF_ENCODING] or None,
|
||||
"job_type": HassJobType.Callback,
|
||||
}
|
||||
|
||||
self._sub_state = subscription.async_prepare_subscribe_topics(
|
||||
self.hass, self._sub_state, topics
|
||||
self.add_subscription(
|
||||
CONF_STATE_TOPIC,
|
||||
self._state_message_received,
|
||||
{
|
||||
"_attr_current_valve_position",
|
||||
"_attr_is_closed",
|
||||
"_attr_is_closing",
|
||||
"_attr_is_opening",
|
||||
},
|
||||
)
|
||||
|
||||
async def _subscribe_topics(self) -> None:
|
||||
|
@@ -281,18 +281,17 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity):
|
||||
assert isinstance(payload, str)
|
||||
self._attr_current_operation = payload
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# add subscriptions for WaterHeaterEntity
|
||||
self.add_subscription(
|
||||
topics,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
self._handle_current_mode_received,
|
||||
{"_attr_current_operation"},
|
||||
)
|
||||
|
||||
self.prepare_subscribe_topics(topics)
|
||||
# add subscriptions for MqttTemperatureControlEntity
|
||||
self.prepare_subscribe_topics()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
|
@@ -15,8 +15,13 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_MODE, CONF_UNIT_OF_MEASUREMENT, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, async_get_hass, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
async_get_hass_or_none,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.config_validation import (
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
@@ -213,10 +218,9 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"value",
|
||||
)
|
||||
):
|
||||
hass: HomeAssistant | None = None
|
||||
with suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
report_issue = async_suggest_report_issue(hass, module=cls.__module__)
|
||||
report_issue = async_suggest_report_issue(
|
||||
async_get_hass_or_none(), module=cls.__module__
|
||||
)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"%s::%s is overriding deprecated methods on an instance of "
|
||||
|
@@ -138,7 +138,7 @@ class OpenAIConversationEntity(
|
||||
api_prompt = await llm_api.async_get_api_prompt(empty_tool_input)
|
||||
|
||||
else:
|
||||
api_prompt = llm.PROMPT_NO_API_CONFIGURED
|
||||
api_prompt = llm.async_render_no_api_prompt(self.hass)
|
||||
|
||||
prompt = "\n".join(
|
||||
(
|
||||
|
@@ -787,10 +787,10 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
display_precision = max(0, display_precision + ratio_log)
|
||||
|
||||
sensor_options: Mapping[str, Any] = self.registry_entry.options.get(DOMAIN, {})
|
||||
if (
|
||||
"suggested_display_precision" in sensor_options
|
||||
and sensor_options["suggested_display_precision"] == display_precision
|
||||
):
|
||||
if "suggested_display_precision" not in sensor_options:
|
||||
if display_precision is None:
|
||||
return
|
||||
elif sensor_options["suggested_display_precision"] == display_precision:
|
||||
return
|
||||
|
||||
registry = er.async_get(self.hass)
|
||||
|
@@ -1,29 +1,28 @@
|
||||
"""Support for The Things network."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
CONF_ACCESS_KEY = "access_key"
|
||||
CONF_APP_ID = "app_id"
|
||||
from .const import CONF_APP_ID, DOMAIN, PLATFORMS, TTN_API_HOST
|
||||
from .coordinator import TTNCoordinator
|
||||
|
||||
DATA_TTN = "data_thethingsnetwork"
|
||||
DOMAIN = "thethingsnetwork"
|
||||
|
||||
TTN_ACCESS_KEY = "ttn_access_key"
|
||||
TTN_APP_ID = "ttn_app_id"
|
||||
TTN_DATA_STORAGE_URL = (
|
||||
"https://{app_id}.data.thethingsnetwork.org/{endpoint}/{device_id}"
|
||||
)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
# Configuration via yaml not longer supported - keeping to warn about migration
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_APP_ID): cv.string,
|
||||
vol.Required(CONF_ACCESS_KEY): cv.string,
|
||||
vol.Required("access_key"): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -33,10 +32,57 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Initialize of The Things Network component."""
|
||||
conf = config[DOMAIN]
|
||||
app_id = conf.get(CONF_APP_ID)
|
||||
access_key = conf.get(CONF_ACCESS_KEY)
|
||||
|
||||
hass.data[DATA_TTN] = {TTN_ACCESS_KEY: access_key, TTN_APP_ID: app_id}
|
||||
if DOMAIN in config:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"manual_migration",
|
||||
breaks_in_ha_version="2024.12.0",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="manual_migration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"v2_v3_migration_url": "https://www.thethingsnetwork.org/forum/c/v2-to-v3-upgrade/102",
|
||||
"v2_deprecation_url": "https://www.thethingsnetwork.org/forum/t/the-things-network-v2-is-permanently-shutting-down-completed/50710",
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Establish connection with The Things Network."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Set up %s at %s",
|
||||
entry.data[CONF_API_KEY],
|
||||
entry.data.get(CONF_HOST, TTN_API_HOST),
|
||||
)
|
||||
|
||||
coordinator = TTNCoordinator(hass, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Remove %s at %s",
|
||||
entry.data[CONF_API_KEY],
|
||||
entry.data.get(CONF_HOST, TTN_API_HOST),
|
||||
)
|
||||
|
||||
# Unload entities created for each supported platform
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return True
|
||||
|
108
homeassistant/components/thethingsnetwork/config_flow.py
Normal file
108
homeassistant/components/thethingsnetwork/config_flow.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""The Things Network's integration config flow."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ttn_client import TTNAuthError, TTNClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import CONF_APP_ID, DOMAIN, TTN_API_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TTNFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""User initiated config flow."""
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
client = TTNClient(
|
||||
user_input[CONF_HOST],
|
||||
user_input[CONF_APP_ID],
|
||||
user_input[CONF_API_KEY],
|
||||
0,
|
||||
)
|
||||
try:
|
||||
await client.fetch_data()
|
||||
except TTNAuthError:
|
||||
_LOGGER.exception("Error authenticating with The Things Network")
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unknown error occurred")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
# Create entry
|
||||
if self._reauth_entry:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._reauth_entry,
|
||||
data=user_input,
|
||||
reason="reauth_successful",
|
||||
)
|
||||
await self.async_set_unique_id(user_input[CONF_APP_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=str(user_input[CONF_APP_ID]),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
# Show form for user to provide settings
|
||||
if not user_input:
|
||||
if self._reauth_entry:
|
||||
user_input = self._reauth_entry.data
|
||||
else:
|
||||
user_input = {CONF_HOST: TTN_API_HOST}
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_APP_ID): str,
|
||||
vol.Required(CONF_API_KEY): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD, autocomplete="api_key"
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, user_input: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by a reauth event."""
|
||||
|
||||
self._reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
12
homeassistant/components/thethingsnetwork/const.py
Normal file
12
homeassistant/components/thethingsnetwork/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""The Things Network's integration constants."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "thethingsnetwork"
|
||||
TTN_API_HOST = "eu1.cloud.thethings.network"
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
CONF_APP_ID = "app_id"
|
||||
|
||||
POLLING_PERIOD_S = 60
|
66
homeassistant/components/thethingsnetwork/coordinator.py
Normal file
66
homeassistant/components/thethingsnetwork/coordinator.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""The Things Network's integration DataUpdateCoordinator."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from ttn_client import TTNAuthError, TTNClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import CONF_APP_ID, POLLING_PERIOD_S
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TTNCoordinator(DataUpdateCoordinator[TTNClient.DATA_TYPE]):
|
||||
"""TTN coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name=f"TheThingsNetwork_{entry.data[CONF_APP_ID]}",
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=timedelta(
|
||||
seconds=POLLING_PERIOD_S,
|
||||
),
|
||||
)
|
||||
|
||||
self._client = TTNClient(
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_APP_ID],
|
||||
entry.data[CONF_API_KEY],
|
||||
push_callback=self._push_callback,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> TTNClient.DATA_TYPE:
|
||||
"""Fetch data from API endpoint.
|
||||
|
||||
This is the place to pre-process the data to lookup tables
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
try:
|
||||
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
measurements = await self._client.fetch_data()
|
||||
except TTNAuthError as err:
|
||||
# Raising ConfigEntryAuthFailed will cancel future updates
|
||||
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
|
||||
_LOGGER.error("TTNAuthError")
|
||||
raise ConfigEntryAuthFailed from err
|
||||
else:
|
||||
# Return measurements
|
||||
_LOGGER.debug("fetched data: %s", measurements)
|
||||
return measurements
|
||||
|
||||
async def _push_callback(self, data: TTNClient.DATA_TYPE) -> None:
|
||||
_LOGGER.debug("pushed data: %s", data)
|
||||
|
||||
# Push data to entities
|
||||
self.async_set_updated_data(data)
|
71
homeassistant/components/thethingsnetwork/entity.py
Normal file
71
homeassistant/components/thethingsnetwork/entity.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Support for The Things Network entities."""
|
||||
|
||||
import logging
|
||||
|
||||
from ttn_client import TTNBaseValue
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TTNCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TTNEntity(CoordinatorEntity[TTNCoordinator]):
|
||||
"""Representation of a The Things Network Data Storage sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_ttn_value: TTNBaseValue
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TTNCoordinator,
|
||||
app_id: str,
|
||||
ttn_value: TTNBaseValue,
|
||||
) -> None:
|
||||
"""Initialize a The Things Network Data Storage sensor."""
|
||||
|
||||
# Pass coordinator to CoordinatorEntity
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._ttn_value = ttn_value
|
||||
|
||||
self._attr_unique_id = f"{self.device_id}_{self.field_id}"
|
||||
self._attr_name = self.field_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{app_id}_{self.device_id}")},
|
||||
name=self.device_id,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
|
||||
my_entity_update = self.coordinator.data.get(self.device_id, {}).get(
|
||||
self.field_id
|
||||
)
|
||||
if (
|
||||
my_entity_update
|
||||
and my_entity_update.received_at > self._ttn_value.received_at
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Received update for %s: %s", self.unique_id, my_entity_update
|
||||
)
|
||||
# Check that the type of an entity has not changed since the creation
|
||||
assert isinstance(my_entity_update, type(self._ttn_value))
|
||||
self._ttn_value = my_entity_update
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
"""Return device_id."""
|
||||
return str(self._ttn_value.device_id)
|
||||
|
||||
@property
|
||||
def field_id(self) -> str:
|
||||
"""Return field_id."""
|
||||
return str(self._ttn_value.field_id)
|
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"domain": "thethingsnetwork",
|
||||
"name": "The Things Network",
|
||||
"codeowners": ["@fabaff"],
|
||||
"codeowners": ["@angelnu"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
|
||||
"iot_class": "local_push"
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["ttn_client==0.0.4"]
|
||||
}
|
||||
|
@@ -1,165 +1,56 @@
|
||||
"""Support for The Things Network's Data storage integration."""
|
||||
"""The Things Network's integration sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import ACCEPT, AUTHORIZATION
|
||||
import voluptuous as vol
|
||||
from ttn_client import TTNSensorValue
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_TIME,
|
||||
CONF_DEVICE_ID,
|
||||
CONTENT_TYPE_JSON,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorEntity, StateType
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DATA_TTN, TTN_ACCESS_KEY, TTN_APP_ID, TTN_DATA_STORAGE_URL
|
||||
from .const import CONF_APP_ID, DOMAIN
|
||||
from .entity import TTNEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_RAW = "raw"
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
CONF_VALUES = "values"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_DEVICE_ID): cv.string,
|
||||
vol.Required(CONF_VALUES): {cv.string: cv.string},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up The Things Network Data storage sensors."""
|
||||
ttn = hass.data[DATA_TTN]
|
||||
device_id = config[CONF_DEVICE_ID]
|
||||
values = config[CONF_VALUES]
|
||||
app_id = ttn.get(TTN_APP_ID)
|
||||
access_key = ttn.get(TTN_ACCESS_KEY)
|
||||
"""Add entities for TTN."""
|
||||
|
||||
ttn_data_storage = TtnDataStorage(hass, app_id, device_id, access_key, values)
|
||||
success = await ttn_data_storage.async_update()
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
if not success:
|
||||
return
|
||||
sensors: set[tuple[str, str]] = set()
|
||||
|
||||
devices = []
|
||||
for value, unit_of_measurement in values.items():
|
||||
devices.append(
|
||||
TtnDataSensor(ttn_data_storage, device_id, value, unit_of_measurement)
|
||||
)
|
||||
async_add_entities(devices, True)
|
||||
def _async_measurement_listener() -> None:
|
||||
data = coordinator.data
|
||||
new_sensors = {
|
||||
(device_id, field_id): TtnDataSensor(
|
||||
coordinator,
|
||||
entry.data[CONF_APP_ID],
|
||||
ttn_value,
|
||||
)
|
||||
for device_id, device_uplinks in data.items()
|
||||
for field_id, ttn_value in device_uplinks.items()
|
||||
if (device_id, field_id) not in sensors
|
||||
and isinstance(ttn_value, TTNSensorValue)
|
||||
}
|
||||
if len(new_sensors):
|
||||
async_add_entities(new_sensors.values())
|
||||
sensors.update(new_sensors.keys())
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_measurement_listener))
|
||||
_async_measurement_listener()
|
||||
|
||||
|
||||
class TtnDataSensor(SensorEntity):
|
||||
"""Representation of a The Things Network Data Storage sensor."""
|
||||
class TtnDataSensor(TTNEntity, SensorEntity):
|
||||
"""Represents a TTN Home Assistant Sensor."""
|
||||
|
||||
def __init__(self, ttn_data_storage, device_id, value, unit_of_measurement):
|
||||
"""Initialize a The Things Network Data Storage sensor."""
|
||||
self._ttn_data_storage = ttn_data_storage
|
||||
self._state = None
|
||||
self._device_id = device_id
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._value = value
|
||||
self._name = f"{self._device_id} {self._value}"
|
||||
_ttn_value: TTNSensorValue
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the entity."""
|
||||
if self._ttn_data_storage.data is not None:
|
||||
try:
|
||||
return self._state[self._value]
|
||||
except KeyError:
|
||||
return None
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
if self._ttn_data_storage.data is not None:
|
||||
return {
|
||||
ATTR_DEVICE_ID: self._device_id,
|
||||
ATTR_RAW: self._state["raw"],
|
||||
ATTR_TIME: self._state["time"],
|
||||
}
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the current state."""
|
||||
await self._ttn_data_storage.async_update()
|
||||
self._state = self._ttn_data_storage.data
|
||||
|
||||
|
||||
class TtnDataStorage:
|
||||
"""Get the latest data from The Things Network Data Storage."""
|
||||
|
||||
def __init__(self, hass, app_id, device_id, access_key, values):
|
||||
"""Initialize the data object."""
|
||||
self.data = None
|
||||
self._hass = hass
|
||||
self._app_id = app_id
|
||||
self._device_id = device_id
|
||||
self._values = values
|
||||
self._url = TTN_DATA_STORAGE_URL.format(
|
||||
app_id=app_id, endpoint="api/v2/query", device_id=device_id
|
||||
)
|
||||
self._headers = {ACCEPT: CONTENT_TYPE_JSON, AUTHORIZATION: f"key {access_key}"}
|
||||
|
||||
async def async_update(self):
|
||||
"""Get the current state from The Things Network Data Storage."""
|
||||
try:
|
||||
session = async_get_clientsession(self._hass)
|
||||
async with asyncio.timeout(DEFAULT_TIMEOUT):
|
||||
response = await session.get(self._url, headers=self._headers)
|
||||
|
||||
except (TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Error while accessing: %s", self._url)
|
||||
return None
|
||||
|
||||
status = response.status
|
||||
|
||||
if status == HTTPStatus.NO_CONTENT:
|
||||
_LOGGER.error("The device is not available: %s", self._device_id)
|
||||
return None
|
||||
|
||||
if status == HTTPStatus.UNAUTHORIZED:
|
||||
_LOGGER.error("Not authorized for Application ID: %s", self._app_id)
|
||||
return None
|
||||
|
||||
if status == HTTPStatus.NOT_FOUND:
|
||||
_LOGGER.error("Application ID is not available: %s", self._app_id)
|
||||
return None
|
||||
|
||||
data = await response.json()
|
||||
self.data = data[-1]
|
||||
|
||||
for value in self._values.items():
|
||||
if value[0] not in self.data:
|
||||
_LOGGER.warning("Value not available: %s", value[0])
|
||||
|
||||
return response
|
||||
return self._ttn_value.value
|
||||
|
32
homeassistant/components/thethingsnetwork/strings.json
Normal file
32
homeassistant/components/thethingsnetwork/strings.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to The Things Network v3 App",
|
||||
"description": "Enter the API hostname, app id and API key for your TTN application.\n\nYou can find your API key in the [The Things Network console](https://console.thethingsnetwork.org) -> Applications -> application_id -> API keys.",
|
||||
"data": {
|
||||
"hostname": "[%key:common::config_flow::data::host%]",
|
||||
"app_id": "Application ID",
|
||||
"access_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The Things Network application could not be connected.\n\nPlease check your credentials."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Application ID is already configured",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"manual_migration": {
|
||||
"description": "Configuring {domain} using YAML was removed as part of migrating to [The Things Network v3]({v2_v3_migration_url}). [The Things Network v2 has shutted down]({v2_deprecation_url}).\n\nPlease remove the {domain} entry from the configuration.yaml and add re-add the integration using the config_flow",
|
||||
"title": "The {domain} YAML configuration is not supported"
|
||||
}
|
||||
}
|
||||
}
|
@@ -39,8 +39,6 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: UnifiConfigEntry
|
||||
) -> bool:
|
||||
"""Set up the UniFi Network integration."""
|
||||
hass.data.setdefault(UNIFI_DOMAIN, {})
|
||||
|
||||
try:
|
||||
api = await get_unifi_api(hass, config_entry.data)
|
||||
|
||||
@@ -62,7 +60,6 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
COMPRESSED_STATE_LAST_UPDATED,
|
||||
COMPRESSED_STATE_STATE,
|
||||
)
|
||||
from homeassistant.core import Event, EventStateChangedData, State
|
||||
from homeassistant.core import CompressedState, Event, EventStateChangedData
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.json import (
|
||||
JSON_DUMP,
|
||||
@@ -177,7 +177,14 @@ def _partial_cached_state_diff_message(event: Event[EventStateChangedData]) -> b
|
||||
)
|
||||
|
||||
|
||||
def _state_diff_event(event: Event[EventStateChangedData]) -> dict:
|
||||
def _state_diff_event(
|
||||
event: Event[EventStateChangedData],
|
||||
) -> dict[
|
||||
str,
|
||||
list[str]
|
||||
| dict[str, CompressedState]
|
||||
| dict[str, dict[str, dict[str, str | list[str]]]],
|
||||
]:
|
||||
"""Convert a state_changed event to the minimal version.
|
||||
|
||||
State update example
|
||||
@@ -188,21 +195,10 @@ def _state_diff_event(event: Event[EventStateChangedData]) -> dict:
|
||||
"r": [entity_id,…]
|
||||
}
|
||||
"""
|
||||
if (event_new_state := event.data["new_state"]) is None:
|
||||
if (new_state := event.data["new_state"]) is None:
|
||||
return {ENTITY_EVENT_REMOVE: [event.data["entity_id"]]}
|
||||
if (event_old_state := event.data["old_state"]) is None:
|
||||
return {
|
||||
ENTITY_EVENT_ADD: {
|
||||
event_new_state.entity_id: event_new_state.as_compressed_state
|
||||
}
|
||||
}
|
||||
return _state_diff(event_old_state, event_new_state)
|
||||
|
||||
|
||||
def _state_diff(
|
||||
old_state: State, new_state: State
|
||||
) -> dict[str, dict[str, dict[str, dict[str, str | list[str]]]]]:
|
||||
"""Create a diff dict that can be used to overlay changes."""
|
||||
if (old_state := event.data["old_state"]) is None:
|
||||
return {ENTITY_EVENT_ADD: {new_state.entity_id: new_state.as_compressed_state}}
|
||||
additions: dict[str, Any] = {}
|
||||
diff: dict[str, dict[str, Any]] = {STATE_DIFF_ADDITIONS: additions}
|
||||
new_state_context = new_state.context
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.48"]
|
||||
"requirements": ["holidays==0.49"]
|
||||
}
|
||||
|
@@ -268,8 +268,16 @@ def async_get_hass() -> HomeAssistant:
|
||||
This should be used where it's very cumbersome or downright impossible to pass
|
||||
hass to the code which needs it.
|
||||
"""
|
||||
if not _hass.hass:
|
||||
if not (hass := async_get_hass_or_none()):
|
||||
raise HomeAssistantError("async_get_hass called from the wrong thread")
|
||||
return hass
|
||||
|
||||
|
||||
def async_get_hass_or_none() -> HomeAssistant | None:
|
||||
"""Return the HomeAssistant instance or None.
|
||||
|
||||
Returns None when called from the wrong thread.
|
||||
"""
|
||||
return _hass.hass
|
||||
|
||||
|
||||
|
@@ -551,6 +551,7 @@ FLOWS = {
|
||||
"tessie",
|
||||
"thermobeacon",
|
||||
"thermopro",
|
||||
"thethingsnetwork",
|
||||
"thread",
|
||||
"tibber",
|
||||
"tile",
|
||||
|
@@ -6146,8 +6146,8 @@
|
||||
"thethingsnetwork": {
|
||||
"name": "The Things Network",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push"
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"thingspeak": {
|
||||
"name": "ThingSpeak",
|
||||
|
@@ -93,8 +93,8 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
async_get_hass,
|
||||
async_get_hass_or_none,
|
||||
split_entity_id,
|
||||
valid_entity_id,
|
||||
)
|
||||
@@ -662,11 +662,7 @@ def template(value: Any | None) -> template_helper.Template:
|
||||
if isinstance(value, (list, dict, template_helper.Template)):
|
||||
raise vol.Invalid("template value should be a string")
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with contextlib.suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
|
||||
template_value = template_helper.Template(str(value), hass)
|
||||
template_value = template_helper.Template(str(value), async_get_hass_or_none())
|
||||
|
||||
try:
|
||||
template_value.ensure_valid()
|
||||
@@ -684,11 +680,7 @@ def dynamic_template(value: Any | None) -> template_helper.Template:
|
||||
if not template_helper.is_template_string(str(value)):
|
||||
raise vol.Invalid("template value does not contain a dynamic template")
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with contextlib.suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
|
||||
template_value = template_helper.Template(str(value), hass)
|
||||
template_value = template_helper.Template(str(value), async_get_hass_or_none())
|
||||
|
||||
try:
|
||||
template_value.ensure_valid()
|
||||
|
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
import functools
|
||||
import inspect
|
||||
@@ -167,8 +166,7 @@ def _print_deprecation_warning_internal(
|
||||
log_when_no_integration_is_found: bool,
|
||||
) -> None:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant.core import HomeAssistant, async_get_hass
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.core import async_get_hass_or_none
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
|
||||
from .frame import MissingIntegrationFrame, get_integration_frame
|
||||
@@ -191,11 +189,8 @@ def _print_deprecation_warning_internal(
|
||||
)
|
||||
else:
|
||||
if integration_frame.custom_integration:
|
||||
hass: HomeAssistant | None = None
|
||||
with suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
report_issue = async_suggest_report_issue(
|
||||
hass,
|
||||
async_get_hass_or_none(),
|
||||
integration_domain=integration_frame.integration,
|
||||
module=integration_frame.module,
|
||||
)
|
||||
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import functools
|
||||
from functools import cached_property
|
||||
@@ -14,7 +13,7 @@ import sys
|
||||
from types import FrameType
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.core import HomeAssistant, async_get_hass
|
||||
from homeassistant.core import async_get_hass_or_none
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import async_suggest_report_issue
|
||||
|
||||
@@ -176,11 +175,8 @@ def _report_integration(
|
||||
return
|
||||
_REPORTED_INTEGRATIONS.add(key)
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
report_issue = async_suggest_report_issue(
|
||||
hass,
|
||||
async_get_hass_or_none(),
|
||||
integration_domain=integration_frame.integration,
|
||||
module=integration_frame.module,
|
||||
)
|
||||
|
@@ -23,10 +23,14 @@ from .singleton import singleton
|
||||
|
||||
LLM_API_ASSIST = "assist"
|
||||
|
||||
PROMPT_NO_API_CONFIGURED = (
|
||||
"Only if the user wants to control a device, tell them to edit the AI configuration "
|
||||
"and allow access to Home Assistant."
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_render_no_api_prompt(hass: HomeAssistant) -> str:
|
||||
"""Return the prompt to be used when no API is configured."""
|
||||
return (
|
||||
"Only if the user wants to control a device, tell them to edit the AI configuration "
|
||||
"and allow access to Home Assistant."
|
||||
)
|
||||
|
||||
|
||||
@singleton("llm")
|
||||
|
@@ -3,15 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import functools
|
||||
import linecache
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, async_get_hass
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.core import async_get_hass_or_none
|
||||
from homeassistant.helpers.frame import (
|
||||
MissingIntegrationFrame,
|
||||
get_current_frame,
|
||||
@@ -74,11 +72,8 @@ def raise_for_blocking_call(
|
||||
f"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
|
||||
)
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
with suppress(HomeAssistantError):
|
||||
hass = async_get_hass()
|
||||
report_issue = async_suggest_report_issue(
|
||||
hass,
|
||||
async_get_hass_or_none(),
|
||||
integration_domain=integration_frame.integration,
|
||||
module=integration_frame.module,
|
||||
)
|
||||
|
10
mypy.ini
10
mypy.ini
@@ -4044,6 +4044,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.thethingsnetwork.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.threshold.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@@ -1084,7 +1084,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.48
|
||||
holidays==0.49
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240501.1
|
||||
@@ -2763,6 +2763,9 @@ transmission-rpc==7.0.3
|
||||
# homeassistant.components.twinkly
|
||||
ttls==1.5.1
|
||||
|
||||
# homeassistant.components.thethingsnetwork
|
||||
ttn_client==0.0.4
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.1.9
|
||||
|
||||
|
@@ -886,7 +886,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.48
|
||||
holidays==0.49
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240501.1
|
||||
@@ -2137,6 +2137,9 @@ transmission-rpc==7.0.3
|
||||
# homeassistant.components.twinkly
|
||||
ttls==1.5.1
|
||||
|
||||
# homeassistant.components.thethingsnetwork
|
||||
ttn_client==0.0.4
|
||||
|
||||
# homeassistant.components.tuya
|
||||
tuya-device-sharing-sdk==0.1.9
|
||||
|
||||
|
@@ -17,8 +17,7 @@ from tests.common import MockConfigEntry
|
||||
def mock_genai():
|
||||
"""Mock the genai call in async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.google_generative_ai_conversation.genai.list_models",
|
||||
return_value=iter([]),
|
||||
"homeassistant.components.google_generative_ai_conversation.genai.get_model"
|
||||
):
|
||||
yield
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from google.api_core.exceptions import ClientError
|
||||
from google.api_core.exceptions import ClientError, DeadlineExceeded
|
||||
from google.rpc.error_details_pb2 import ErrorInfo
|
||||
import pytest
|
||||
|
||||
@@ -69,7 +69,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
assert not result["errors"]
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -186,13 +186,16 @@ async def test_options_switching(
|
||||
("side_effect", "error"),
|
||||
[
|
||||
(
|
||||
ClientError(message="some error"),
|
||||
ClientError("some error"),
|
||||
"cannot_connect",
|
||||
),
|
||||
(
|
||||
DeadlineExceeded("deadline exceeded"),
|
||||
"cannot_connect",
|
||||
),
|
||||
(
|
||||
ClientError(
|
||||
message="invalid api key",
|
||||
error_info=ErrorInfo(reason="API_KEY_INVALID"),
|
||||
"invalid api key", error_info=ErrorInfo(reason="API_KEY_INVALID")
|
||||
),
|
||||
"invalid_auth",
|
||||
),
|
||||
@@ -218,3 +221,51 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error) -> None:
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": error}
|
||||
|
||||
|
||||
async def test_reauth_flow(hass: HomeAssistant) -> None:
|
||||
"""Test the reauth flow."""
|
||||
hass.config.components.add("google_generative_ai_conversation")
|
||||
mock_config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, state=config_entries.ConfigEntryState.LOADED, title="Gemini"
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry.async_start_reauth(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
result = flows[0]
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["context"]["source"] == "reauth"
|
||||
assert result["context"]["title_placeholders"] == {"name": "Gemini"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "api"
|
||||
assert "api_key" in result["data_schema"].schema
|
||||
assert not result["errors"]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.google_generative_ai_conversation.config_flow.genai.list_models",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.google_generative_ai_conversation.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
patch(
|
||||
"homeassistant.components.google_generative_ai_conversation.async_unload_entry",
|
||||
return_value=True,
|
||||
) as mock_unload_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"api_key": "1234"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert hass.config_entries.async_entries(DOMAIN)[0].data == {"api_key": "1234"}
|
||||
assert len(mock_unload_entry.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
@@ -2,7 +2,8 @@
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from google.api_core.exceptions import ClientError
|
||||
from google.api_core.exceptions import ClientError, DeadlineExceeded
|
||||
from google.rpc.error_details_pb2 import ErrorInfo
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -220,29 +221,39 @@ async def test_generate_content_service_with_non_image(
|
||||
)
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "state", "reauth"),
|
||||
[
|
||||
(
|
||||
ClientError("some error"),
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
False,
|
||||
),
|
||||
(
|
||||
DeadlineExceeded("deadline exceeded"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
False,
|
||||
),
|
||||
(
|
||||
ClientError(
|
||||
"invalid api key", error_info=ErrorInfo(reason="API_KEY_INVALID")
|
||||
),
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_config_entry_error(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, side_effect, state, reauth
|
||||
) -> None:
|
||||
"""Test configuration entry not ready."""
|
||||
"""Test different configuration entry errors."""
|
||||
with patch(
|
||||
"homeassistant.components.google_generative_ai_conversation.genai.list_models",
|
||||
side_effect=ClientError("error"),
|
||||
"homeassistant.components.google_generative_ai_conversation.genai.get_model",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_config_entry_setup_error(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test configuration entry setup error."""
|
||||
with patch(
|
||||
"homeassistant.components.google_generative_ai_conversation.genai.list_models",
|
||||
side_effect=ClientError("error", error_info="API_KEY_INVALID"),
|
||||
):
|
||||
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
|
||||
assert mock_config_entry.state is state
|
||||
mock_config_entry.async_get_active_flows(hass, {"reauth"})
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {"reauth"})) is reauth
|
||||
|
@@ -27,11 +27,6 @@ async def setup_repairs(hass):
|
||||
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def mock_all(all_setup_requests):
|
||||
"""Mock all setup requests."""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def fixture_supervisor_environ():
|
||||
"""Mock os environ for supervisor."""
|
||||
@@ -110,9 +105,13 @@ def assert_issue_repair_in_list(
|
||||
context: str,
|
||||
type_: str,
|
||||
fixable: bool,
|
||||
reference: str | None,
|
||||
*,
|
||||
reference: str | None = None,
|
||||
placeholders: dict[str, str] | None = None,
|
||||
):
|
||||
"""Assert repair for unhealthy/unsupported in list."""
|
||||
if reference:
|
||||
placeholders = (placeholders or {}) | {"reference": reference}
|
||||
assert {
|
||||
"breaks_in_ha_version": None,
|
||||
"created": ANY,
|
||||
@@ -125,7 +124,7 @@ def assert_issue_repair_in_list(
|
||||
"learn_more_url": None,
|
||||
"severity": "warning",
|
||||
"translation_key": f"issue_{context}_{type_}",
|
||||
"translation_placeholders": {"reference": reference} if reference else None,
|
||||
"translation_placeholders": placeholders,
|
||||
} in issues
|
||||
|
||||
|
||||
@@ -133,6 +132,7 @@ async def test_unhealthy_issues(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test issues added for unhealthy systems."""
|
||||
mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"])
|
||||
@@ -154,6 +154,7 @@ async def test_unsupported_issues(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test issues added for unsupported systems."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"])
|
||||
@@ -177,6 +178,7 @@ async def test_unhealthy_issues_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test unhealthy issues added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
@@ -233,6 +235,7 @@ async def test_unsupported_issues_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test unsupported issues added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
@@ -289,6 +292,7 @@ async def test_reset_issues_supervisor_restart(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""All issues reset on supervisor restart."""
|
||||
mock_resolution_info(
|
||||
@@ -352,6 +356,7 @@ async def test_reasons_added_and_removed(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test an unsupported/unhealthy reasons being added and removed at same time."""
|
||||
mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"])
|
||||
@@ -401,6 +406,7 @@ async def test_ignored_unsupported_skipped(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Unsupported reasons which have an identical unhealthy reason are ignored."""
|
||||
mock_resolution_info(
|
||||
@@ -423,6 +429,7 @@ async def test_new_unsupported_unhealthy_reason(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""New unsupported/unhealthy reasons result in a generic repair until next core update."""
|
||||
mock_resolution_info(
|
||||
@@ -472,6 +479,7 @@ async def test_supervisor_issues(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test repairs added for supervisor issue."""
|
||||
mock_resolution_info(
|
||||
@@ -538,6 +546,7 @@ async def test_supervisor_issues_initial_failure(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test issues manager retries after initial update failure."""
|
||||
responses = [
|
||||
@@ -614,6 +623,7 @@ async def test_supervisor_issues_add_remove(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test supervisor issues added and removed from dispatches."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
@@ -724,6 +734,7 @@ async def test_supervisor_issues_suggestions_fail(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test failing to get suggestions for issue skips it."""
|
||||
aioclient_mock.get(
|
||||
@@ -769,6 +780,7 @@ async def test_supervisor_remove_missing_issue_without_error(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test HA skips message to remove issue that it didn't know about (sync issue)."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
@@ -802,6 +814,7 @@ async def test_system_is_not_ready(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Ensure hassio starts despite error."""
|
||||
aioclient_mock.get(
|
||||
@@ -814,3 +827,57 @@ async def test_system_is_not_ready(
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
assert "Failed to update supervisor issues" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"all_setup_requests", [{"include_addons": True}], indirect=True
|
||||
)
|
||||
async def test_supervisor_issues_detached_addon_missing(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test supervisor issue for detached addon due to missing repository."""
|
||||
mock_resolution_info(aioclient_mock)
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
{
|
||||
"id": 1,
|
||||
"type": "supervisor/event",
|
||||
"data": {
|
||||
"event": "issue_changed",
|
||||
"data": {
|
||||
"uuid": "1234",
|
||||
"type": "detached_addon_missing",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await client.send_json({"id": 2, "type": "repairs/list_issues"})
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert len(msg["result"]["issues"]) == 1
|
||||
assert_issue_repair_in_list(
|
||||
msg["result"]["issues"],
|
||||
uuid="1234",
|
||||
context="addon",
|
||||
type_="detached_addon_missing",
|
||||
fixable=False,
|
||||
placeholders={
|
||||
"reference": "test",
|
||||
"addon": "test",
|
||||
"addon_url": "https://github.com/home-assistant/addons/test",
|
||||
},
|
||||
)
|
||||
|
@@ -780,3 +780,90 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks(
|
||||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1236"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"all_setup_requests", [{"include_addons": True}], indirect=True
|
||||
)
|
||||
async def test_supervisor_issue_detached_addon_removed(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
all_setup_requests,
|
||||
) -> None:
|
||||
"""Test fix flow for supervisor issue."""
|
||||
mock_resolution_info(
|
||||
aioclient_mock,
|
||||
issues=[
|
||||
{
|
||||
"uuid": "1234",
|
||||
"type": "detached_addon_removed",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
"suggestions": [
|
||||
{
|
||||
"uuid": "1235",
|
||||
"type": "execute_remove",
|
||||
"context": "addon",
|
||||
"reference": "test",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
|
||||
repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
assert repair_issue
|
||||
|
||||
client = await hass_client()
|
||||
|
||||
resp = await client.post(
|
||||
"/api/repairs/issues/fix",
|
||||
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
|
||||
)
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "form",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"step_id": "addon_execute_remove",
|
||||
"data_schema": [],
|
||||
"errors": None,
|
||||
"description_placeholders": {
|
||||
"reference": "test",
|
||||
"addon": "test",
|
||||
"help_url": "https://www.home-assistant.io/help/",
|
||||
"community_url": "https://community.home-assistant.io/",
|
||||
},
|
||||
"last_step": True,
|
||||
"preview": None,
|
||||
}
|
||||
|
||||
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
|
||||
|
||||
assert resp.status == HTTPStatus.OK
|
||||
data = await resp.json()
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data == {
|
||||
"type": "create_entry",
|
||||
"flow_id": flow_id,
|
||||
"handler": "hassio",
|
||||
"description": None,
|
||||
"description_placeholders": None,
|
||||
}
|
||||
|
||||
assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234")
|
||||
|
||||
assert aioclient_mock.mock_calls[-1][0] == "post"
|
||||
assert (
|
||||
str(aioclient_mock.mock_calls[-1][1])
|
||||
== "http://127.0.0.1/resolution/suggestion/1235"
|
||||
)
|
||||
|
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.jewish_calendar import config_flow
|
||||
from homeassistant.components.jewish_calendar.const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -14,8 +14,8 @@ from tests.common import MockConfigEntry
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title=config_flow.DEFAULT_NAME,
|
||||
domain=config_flow.DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
domain=DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -5,11 +5,10 @@ from unittest.mock import AsyncMock
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.jewish_calendar import (
|
||||
from homeassistant.components.jewish_calendar.const import (
|
||||
CONF_CANDLE_LIGHT_MINUTES,
|
||||
CONF_DIASPORA,
|
||||
CONF_HAVDALAH_OFFSET_MINUTES,
|
||||
CONF_LANGUAGE,
|
||||
DEFAULT_CANDLE_LIGHT,
|
||||
DEFAULT_DIASPORA,
|
||||
DEFAULT_HAVDALAH_OFFSET_MINUTES,
|
||||
@@ -19,6 +18,7 @@ from homeassistant.components.jewish_calendar import (
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import (
|
||||
CONF_ELEVATION,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
|
@@ -4,8 +4,8 @@ from datetime import datetime as dt, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import jewish_calendar
|
||||
from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.jewish_calendar.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -17,7 +17,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None:
|
||||
"""Test minimum jewish calendar configuration."""
|
||||
entry = MockConfigEntry(domain=jewish_calendar.DOMAIN, data={})
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -26,7 +26,7 @@ async def test_jewish_calendar_min_config(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_jewish_calendar_hebrew(hass: HomeAssistant) -> None:
|
||||
"""Test jewish calendar sensor with language set to hebrew."""
|
||||
entry = MockConfigEntry(domain=jewish_calendar.DOMAIN, data={"language": "hebrew"})
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={"language": "hebrew"})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -167,7 +167,7 @@ async def test_jewish_calendar_sensor(
|
||||
|
||||
with alter_time(test_time):
|
||||
entry = MockConfigEntry(
|
||||
domain=jewish_calendar.DOMAIN,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"language": language,
|
||||
"diaspora": diaspora,
|
||||
@@ -509,7 +509,7 @@ async def test_shabbat_times_sensor(
|
||||
|
||||
with alter_time(test_time):
|
||||
entry = MockConfigEntry(
|
||||
domain=jewish_calendar.DOMAIN,
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"language": language,
|
||||
"diaspora": diaspora,
|
||||
@@ -566,7 +566,7 @@ async def test_omer_sensor(hass: HomeAssistant, test_time, result) -> None:
|
||||
test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone))
|
||||
|
||||
with alter_time(test_time):
|
||||
entry = MockConfigEntry(domain=jewish_calendar.DOMAIN)
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -600,7 +600,7 @@ async def test_dafyomi_sensor(hass: HomeAssistant, test_time, result) -> None:
|
||||
test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone))
|
||||
|
||||
with alter_time(test_time):
|
||||
entry = MockConfigEntry(domain=jewish_calendar.DOMAIN)
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -620,7 +620,7 @@ async def test_no_discovery_info(
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
{SENSOR_DOMAIN: {"platform": jewish_calendar.DOMAIN}},
|
||||
{SENSOR_DOMAIN: {"platform": DOMAIN}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert SENSOR_DOMAIN in hass.config.components
|
||||
|
@@ -16,7 +16,7 @@ import yaml
|
||||
from homeassistant import config as module_hass_config
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.mqtt import debug_info
|
||||
from homeassistant.components.mqtt.const import MQTT_DISCONNECTED
|
||||
from homeassistant.components.mqtt.const import MQTT_CONNECTION_STATE
|
||||
from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED
|
||||
from homeassistant.components.mqtt.models import PublishPayloadType
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -115,7 +115,7 @@ async def help_test_availability_when_connection_lost(
|
||||
assert state and state.state != STATE_UNAVAILABLE
|
||||
|
||||
mqtt_mock.connected = False
|
||||
async_dispatcher_send(hass, MQTT_DISCONNECTED)
|
||||
async_dispatcher_send(hass, MQTT_CONNECTION_STATE, False)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(f"{domain}.test")
|
||||
|
10
tests/components/thethingsnetwork/__init__.py
Normal file
10
tests/components/thethingsnetwork/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Define tests for the The Things Network."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def init_integration(hass: HomeAssistant, config_entry) -> None:
|
||||
"""Mock TTNClient."""
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
95
tests/components/thethingsnetwork/conftest.py
Normal file
95
tests/components/thethingsnetwork/conftest.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Define fixtures for the The Things Network tests."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from ttn_client import TTNSensorValue
|
||||
|
||||
from homeassistant.components.thethingsnetwork.const import (
|
||||
CONF_APP_ID,
|
||||
DOMAIN,
|
||||
TTN_API_HOST,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
HOST = "example.com"
|
||||
APP_ID = "my_app"
|
||||
API_KEY = "my_api_key"
|
||||
|
||||
DEVICE_ID = "my_device"
|
||||
DEVICE_ID_2 = "my_device_2"
|
||||
DEVICE_FIELD = "a_field"
|
||||
DEVICE_FIELD_2 = "a_field_2"
|
||||
DEVICE_FIELD_VALUE = 42
|
||||
|
||||
DATA = {
|
||||
DEVICE_ID: {
|
||||
DEVICE_FIELD: TTNSensorValue(
|
||||
{
|
||||
"end_device_ids": {"device_id": DEVICE_ID},
|
||||
"received_at": "2024-03-11T08:49:11.153738893Z",
|
||||
},
|
||||
DEVICE_FIELD,
|
||||
DEVICE_FIELD_VALUE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DATA_UPDATE = {
|
||||
DEVICE_ID: {
|
||||
DEVICE_FIELD: TTNSensorValue(
|
||||
{
|
||||
"end_device_ids": {"device_id": DEVICE_ID},
|
||||
"received_at": "2024-03-12T08:49:11.153738893Z",
|
||||
},
|
||||
DEVICE_FIELD,
|
||||
DEVICE_FIELD_VALUE,
|
||||
)
|
||||
},
|
||||
DEVICE_ID_2: {
|
||||
DEVICE_FIELD_2: TTNSensorValue(
|
||||
{
|
||||
"end_device_ids": {"device_id": DEVICE_ID_2},
|
||||
"received_at": "2024-03-12T08:49:11.153738893Z",
|
||||
},
|
||||
DEVICE_FIELD_2,
|
||||
DEVICE_FIELD_VALUE,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=APP_ID,
|
||||
title=APP_ID,
|
||||
data={
|
||||
CONF_APP_ID: APP_ID,
|
||||
CONF_HOST: TTN_API_HOST,
|
||||
CONF_API_KEY: API_KEY,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ttnclient():
|
||||
"""Mock TTNClient."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.thethingsnetwork.coordinator.TTNClient",
|
||||
autospec=True,
|
||||
) as ttn_client,
|
||||
patch(
|
||||
"homeassistant.components.thethingsnetwork.config_flow.TTNClient",
|
||||
new=ttn_client,
|
||||
),
|
||||
):
|
||||
instance = ttn_client.return_value
|
||||
instance.fetch_data = AsyncMock(return_value=DATA)
|
||||
yield ttn_client
|
132
tests/components/thethingsnetwork/test_config_flow.py
Normal file
132
tests/components/thethingsnetwork/test_config_flow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Define tests for the The Things Network onfig flows."""
|
||||
|
||||
import pytest
|
||||
from ttn_client import TTNAuthError
|
||||
|
||||
from homeassistant.components.thethingsnetwork.const import CONF_APP_ID, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import init_integration
|
||||
from .conftest import API_KEY, APP_ID, HOST
|
||||
|
||||
USER_DATA = {CONF_HOST: HOST, CONF_APP_ID: APP_ID, CONF_API_KEY: API_KEY}
|
||||
|
||||
|
||||
async def test_user(hass: HomeAssistant, mock_ttnclient) -> None:
|
||||
"""Test user config."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == APP_ID
|
||||
assert result["data"][CONF_HOST] == HOST
|
||||
assert result["data"][CONF_APP_ID] == APP_ID
|
||||
assert result["data"][CONF_API_KEY] == API_KEY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fetch_data_exception", "base_error"),
|
||||
[(TTNAuthError, "invalid_auth"), (Exception, "unknown")],
|
||||
)
|
||||
async def test_user_errors(
|
||||
hass: HomeAssistant, fetch_data_exception, base_error, mock_ttnclient
|
||||
) -> None:
|
||||
"""Test user config errors."""
|
||||
|
||||
# Test error
|
||||
mock_ttnclient.return_value.fetch_data.side_effect = fetch_data_exception
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert base_error in result["errors"]["base"]
|
||||
|
||||
# Recover
|
||||
mock_ttnclient.return_value.fetch_data.side_effect = None
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_duplicate_entry(
|
||||
hass: HomeAssistant, mock_ttnclient, mock_config_entry
|
||||
) -> None:
|
||||
"""Test that duplicate entries are caught."""
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_step_reauth(
|
||||
hass: HomeAssistant, mock_ttnclient, mock_config_entry
|
||||
) -> None:
|
||||
"""Test that the reauth step works."""
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"unique_id": APP_ID,
|
||||
"entry_id": mock_config_entry.entry_id,
|
||||
},
|
||||
data=USER_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert not result["errors"]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
|
||||
new_api_key = "1234"
|
||||
new_user_input = dict(USER_DATA)
|
||||
new_user_input[CONF_API_KEY] = new_api_key
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input=new_user_input
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key
|
33
tests/components/thethingsnetwork/test_init.py
Normal file
33
tests/components/thethingsnetwork/test_init.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Define tests for the The Things Network init."""
|
||||
|
||||
import pytest
|
||||
from ttn_client import TTNAuthError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .conftest import DOMAIN
|
||||
|
||||
|
||||
async def test_error_configuration(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test issue is logged when deprecated configuration is used."""
|
||||
await async_setup_component(
|
||||
hass, DOMAIN, {DOMAIN: {"app_id": "123", "access_key": "42"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert issue_registry.async_get_issue(DOMAIN, "manual_migration")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("exception_class"), [TTNAuthError, Exception])
|
||||
async def test_init_exceptions(
|
||||
hass: HomeAssistant, mock_ttnclient, exception_class, mock_config_entry
|
||||
) -> None:
|
||||
"""Test TTN Exceptions."""
|
||||
|
||||
mock_ttnclient.return_value.fetch_data.side_effect = exception_class
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
43
tests/components/thethingsnetwork/test_sensor.py
Normal file
43
tests/components/thethingsnetwork/test_sensor.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Define tests for the The Things Network sensor."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import init_integration
|
||||
from .conftest import (
|
||||
APP_ID,
|
||||
DATA_UPDATE,
|
||||
DEVICE_FIELD,
|
||||
DEVICE_FIELD_2,
|
||||
DEVICE_ID,
|
||||
DEVICE_ID_2,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def test_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_ttnclient,
|
||||
mock_config_entry,
|
||||
) -> None:
|
||||
"""Test a working configurations."""
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Check devices
|
||||
assert (
|
||||
device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, f"{APP_ID}_{DEVICE_ID}")}
|
||||
).name
|
||||
== DEVICE_ID
|
||||
)
|
||||
|
||||
# Check entities
|
||||
assert entity_registry.async_get(f"sensor.{DEVICE_ID}_{DEVICE_FIELD}")
|
||||
|
||||
assert not entity_registry.async_get(f"sensor.{DEVICE_ID_2}_{DEVICE_FIELD}")
|
||||
push_callback = mock_ttnclient.call_args.kwargs["push_callback"]
|
||||
await push_callback(DATA_UPDATE)
|
||||
assert entity_registry.async_get(f"sensor.{DEVICE_ID_2}_{DEVICE_FIELD_2}")
|
@@ -3,22 +3,265 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiounifi.models.message import MessageKey
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN
|
||||
from homeassistant.components.unifi.hub.websocket import RETRY_TIMER
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
CONTENT_TYPE_JSON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.unifi.test_hub import DEFAULT_CONFIG_ENTRY_ID
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
DEFAULT_CONFIG_ENTRY_ID = "1"
|
||||
DEFAULT_HOST = "1.2.3.4"
|
||||
DEFAULT_PORT = 1234
|
||||
DEFAULT_SITE = "site_id"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_discovery():
|
||||
"""No real network traffic allowed."""
|
||||
with patch(
|
||||
"homeassistant.components.unifi.config_flow._async_discover_unifi",
|
||||
return_value=None,
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device_registry(hass, device_registry: dr.DeviceRegistry):
|
||||
"""Mock device registry."""
|
||||
config_entry = MockConfigEntry(domain="something_else")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
for idx, device in enumerate(
|
||||
(
|
||||
"00:00:00:00:00:01",
|
||||
"00:00:00:00:00:02",
|
||||
"00:00:00:00:00:03",
|
||||
"00:00:00:00:00:04",
|
||||
"00:00:00:00:00:05",
|
||||
"00:00:00:00:00:06",
|
||||
"00:00:00:00:01:01",
|
||||
"00:00:00:00:02:02",
|
||||
)
|
||||
):
|
||||
device_registry.async_get_or_create(
|
||||
name=f"Device {idx}",
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device)},
|
||||
)
|
||||
|
||||
|
||||
# Config entry fixtures
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def config_entry_fixture(
|
||||
hass: HomeAssistant,
|
||||
config_entry_data: MappingProxyType[str, Any],
|
||||
config_entry_options: MappingProxyType[str, Any],
|
||||
) -> ConfigEntry:
|
||||
"""Define a config entry fixture."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=UNIFI_DOMAIN,
|
||||
entry_id="1",
|
||||
unique_id="1",
|
||||
data=config_entry_data,
|
||||
options=config_entry_options,
|
||||
version=1,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry_data")
|
||||
def config_entry_data_fixture() -> MappingProxyType[str, Any]:
|
||||
"""Define a config entry data fixture."""
|
||||
return {
|
||||
CONF_HOST: DEFAULT_HOST,
|
||||
CONF_USERNAME: "username",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_PORT: DEFAULT_PORT,
|
||||
CONF_SITE_ID: DEFAULT_SITE,
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry_options")
|
||||
def config_entry_options_fixture() -> MappingProxyType[str, Any]:
|
||||
"""Define a config entry options fixture."""
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_unifi_requests")
|
||||
def default_request_fixture(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
client_payload: list[dict[str, Any]],
|
||||
clients_all_payload: list[dict[str, Any]],
|
||||
device_payload: list[dict[str, Any]],
|
||||
dpi_app_payload: list[dict[str, Any]],
|
||||
dpi_group_payload: list[dict[str, Any]],
|
||||
port_forward_payload: list[dict[str, Any]],
|
||||
site_payload: list[dict[str, Any]],
|
||||
system_information_payload: list[dict[str, Any]],
|
||||
wlan_payload: list[dict[str, Any]],
|
||||
) -> Callable[[str], None]:
|
||||
"""Mock default UniFi requests responses."""
|
||||
|
||||
def __mock_default_requests(host: str, site_id: str) -> None:
|
||||
url = f"https://{host}:{DEFAULT_PORT}"
|
||||
|
||||
def mock_get_request(path: str, payload: list[dict[str, Any]]) -> None:
|
||||
aioclient_mock.get(
|
||||
f"{url}{path}",
|
||||
json={"meta": {"rc": "OK"}, "data": payload},
|
||||
headers={"content-type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
|
||||
aioclient_mock.get(url, status=302) # UniFI OS check
|
||||
aioclient_mock.post(
|
||||
f"{url}/api/login",
|
||||
json={"data": "login successful", "meta": {"rc": "ok"}},
|
||||
headers={"content-type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
mock_get_request("/api/self/sites", site_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/stat/sta", client_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/rest/user", clients_all_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/stat/device", device_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/rest/dpiapp", dpi_app_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/rest/dpigroup", dpi_group_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/rest/portforward", port_forward_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/stat/sysinfo", system_information_payload)
|
||||
mock_get_request(f"/api/s/{site_id}/rest/wlanconf", wlan_payload)
|
||||
|
||||
return __mock_default_requests
|
||||
|
||||
|
||||
# Request payload fixtures
|
||||
|
||||
|
||||
@pytest.fixture(name="client_payload")
|
||||
def client_data_fixture() -> list[dict[str, Any]]:
|
||||
"""Client data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="clients_all_payload")
|
||||
def clients_all_data_fixture() -> list[dict[str, Any]]:
|
||||
"""Clients all data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="device_payload")
|
||||
def device_data_fixture() -> list[dict[str, Any]]:
|
||||
"""Device data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="dpi_app_payload")
|
||||
def dpi_app_data_fixture() -> list[dict[str, Any]]:
|
||||
"""DPI app data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="dpi_group_payload")
|
||||
def dpi_group_data_fixture() -> list[dict[str, Any]]:
|
||||
"""DPI group data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="port_forward_payload")
|
||||
def port_forward_data_fixture() -> list[dict[str, Any]]:
|
||||
"""Port forward data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="site_payload")
|
||||
def site_data_fixture() -> list[dict[str, Any]]:
|
||||
"""Site data."""
|
||||
return [{"desc": "Site name", "name": "site_id", "role": "admin", "_id": "1"}]
|
||||
|
||||
|
||||
@pytest.fixture(name="system_information_payload")
|
||||
def system_information_data_fixture() -> list[dict[str, Any]]:
|
||||
"""System information data."""
|
||||
return [
|
||||
{
|
||||
"anonymous_controller_id": "24f81231-a456-4c32-abcd-f5612345385f",
|
||||
"build": "atag_7.4.162_21057",
|
||||
"console_display_version": "3.1.15",
|
||||
"hostname": "UDMP",
|
||||
"name": "UDMP",
|
||||
"previous_version": "7.4.156",
|
||||
"timezone": "Europe/Stockholm",
|
||||
"ubnt_device_type": "UDMPRO",
|
||||
"udm_version": "3.0.20.9281",
|
||||
"update_available": False,
|
||||
"update_downloaded": False,
|
||||
"uptime": 1196290,
|
||||
"version": "7.4.162",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(name="wlan_payload")
|
||||
def wlan_data_fixture() -> list[dict[str, Any]]:
|
||||
"""WLAN data."""
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture(name="setup_default_unifi_requests")
|
||||
def default_vapix_requests_fixture(
|
||||
config_entry: ConfigEntry,
|
||||
mock_unifi_requests: Callable[[str, str], None],
|
||||
) -> None:
|
||||
"""Mock default UniFi requests responses."""
|
||||
mock_unifi_requests(config_entry.data[CONF_HOST], config_entry.data[CONF_SITE_ID])
|
||||
|
||||
|
||||
@pytest.fixture(name="prepare_config_entry")
|
||||
async def prep_config_entry_fixture(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, setup_default_unifi_requests: None
|
||||
) -> Callable[[], ConfigEntry]:
|
||||
"""Fixture factory to set up UniFi network integration."""
|
||||
|
||||
async def __mock_setup_config_entry() -> ConfigEntry:
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return config_entry
|
||||
|
||||
return __mock_setup_config_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="setup_config_entry")
|
||||
async def setup_config_entry_fixture(
|
||||
hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry]
|
||||
) -> ConfigEntry:
|
||||
"""Fixture to set up UniFi network integration."""
|
||||
return await prepare_config_entry()
|
||||
|
||||
|
||||
# Websocket fixtures
|
||||
|
||||
|
||||
class WebsocketStateManager(asyncio.Event):
|
||||
"""Keep an async event that simules websocket context manager.
|
||||
@@ -97,38 +340,3 @@ def mock_unifi_websocket(hass):
|
||||
raise NotImplementedError
|
||||
|
||||
return make_websocket_call
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_discovery():
|
||||
"""No real network traffic allowed."""
|
||||
with patch(
|
||||
"homeassistant.components.unifi.config_flow._async_discover_unifi",
|
||||
return_value=None,
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device_registry(hass, device_registry: dr.DeviceRegistry):
|
||||
"""Mock device registry."""
|
||||
config_entry = MockConfigEntry(domain="something_else")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
for idx, device in enumerate(
|
||||
(
|
||||
"00:00:00:00:00:01",
|
||||
"00:00:00:00:00:02",
|
||||
"00:00:00:00:00:03",
|
||||
"00:00:00:00:00:04",
|
||||
"00:00:00:00:00:05",
|
||||
"00:00:00:00:00:06",
|
||||
"00:00:00:00:01:01",
|
||||
"00:00:00:00:02:02",
|
||||
)
|
||||
):
|
||||
device_registry.async_get_or_create(
|
||||
name=f"Device {idx}",
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device)},
|
||||
)
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, ButtonDeviceClass
|
||||
from homeassistant.components.unifi.const import CONF_SITE_ID
|
||||
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
|
||||
@@ -17,8 +19,6 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .test_hub import setup_unifi_integration
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
@@ -60,17 +60,10 @@ WLAN = {
|
||||
}
|
||||
|
||||
|
||||
async def test_restart_device_button(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Test restarting device button."""
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass,
|
||||
aioclient_mock,
|
||||
devices_response=[
|
||||
@pytest.mark.parametrize(
|
||||
"device_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"board_rev": 3,
|
||||
"device_id": "mock-id",
|
||||
@@ -83,8 +76,18 @@ async def test_restart_device_button(
|
||||
"type": "usw",
|
||||
"version": "4.0.42.10433",
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_restart_device_button(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_config_entry,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Test restarting device button."""
|
||||
config_entry = setup_config_entry
|
||||
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 1
|
||||
|
||||
ent_reg_entry = entity_registry.async_get("button.switch_restart")
|
||||
@@ -127,17 +130,10 @@ async def test_restart_device_button(
|
||||
assert hass.states.get("button.switch_restart").state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_power_cycle_poe(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Test restarting device button."""
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass,
|
||||
aioclient_mock,
|
||||
devices_response=[
|
||||
@pytest.mark.parametrize(
|
||||
"device_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"board_rev": 3,
|
||||
"device_id": "mock-id",
|
||||
@@ -166,8 +162,18 @@ async def test_power_cycle_poe(
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_power_cycle_poe(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_config_entry,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Test restarting device button."""
|
||||
config_entry = setup_config_entry
|
||||
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 2
|
||||
|
||||
ent_reg_entry = entity_registry.async_get("button.switch_port_1_power_cycle")
|
||||
@@ -214,17 +220,16 @@ async def test_power_cycle_poe(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wlan_payload", [[WLAN]])
|
||||
async def test_wlan_regenerate_password(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_config_entry,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Test WLAN regenerate password button."""
|
||||
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass, aioclient_mock, wlans_response=[WLAN]
|
||||
)
|
||||
config_entry = setup_config_entry
|
||||
assert len(hass.states.async_entity_ids(BUTTON_DOMAIN)) == 0
|
||||
|
||||
button_regenerate_password = "button.ssid_1_regenerate_password"
|
||||
|
@@ -1,9 +1,10 @@
|
||||
"""Test UniFi Network."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import aiounifi
|
||||
import pytest
|
||||
@@ -15,8 +16,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.unifi.const import (
|
||||
CONF_SITE_ID,
|
||||
CONF_TRACK_CLIENTS,
|
||||
CONF_TRACK_DEVICES,
|
||||
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
|
||||
DEFAULT_ALLOW_UPTIME_SENSORS,
|
||||
DEFAULT_DETECTION_TIME,
|
||||
@@ -29,6 +28,7 @@ from homeassistant.components.unifi.const import (
|
||||
from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect
|
||||
from homeassistant.components.unifi.hub import get_unifi_api
|
||||
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -39,7 +39,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -239,18 +238,15 @@ async def setup_unifi_integration(
|
||||
|
||||
|
||||
async def test_hub_setup(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
prepare_config_entry: Callable[[], ConfigEntry],
|
||||
) -> None:
|
||||
"""Successful setup."""
|
||||
with patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setups",
|
||||
return_value=True,
|
||||
) as forward_entry_setup:
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass, aioclient_mock, system_information_response=SYSTEM_INFORMATION
|
||||
)
|
||||
config_entry = await prepare_config_entry()
|
||||
hub = config_entry.runtime_data
|
||||
|
||||
entry = hub.config.entry
|
||||
@@ -291,109 +287,53 @@ async def test_hub_setup(
|
||||
assert device_entry.sw_version == "7.4.162"
|
||||
|
||||
|
||||
async def test_hub_not_accessible(hass: HomeAssistant) -> None:
|
||||
"""Retry to login gets scheduled when connection fails."""
|
||||
with patch(
|
||||
"homeassistant.components.unifi.hub.get_unifi_api",
|
||||
side_effect=CannotConnect,
|
||||
):
|
||||
await setup_unifi_integration(hass)
|
||||
assert hass.data[UNIFI_DOMAIN] == {}
|
||||
|
||||
|
||||
async def test_hub_trigger_reauth_flow(hass: HomeAssistant) -> None:
|
||||
"""Failed authentication trigger a reauthentication flow."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.unifi.get_unifi_api",
|
||||
side_effect=AuthenticationRequired,
|
||||
),
|
||||
patch.object(hass.config_entries.flow, "async_init") as mock_flow_init,
|
||||
):
|
||||
await setup_unifi_integration(hass)
|
||||
mock_flow_init.assert_called_once()
|
||||
assert hass.data[UNIFI_DOMAIN] == {}
|
||||
|
||||
|
||||
async def test_hub_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Unknown errors are handled."""
|
||||
with patch(
|
||||
"homeassistant.components.unifi.hub.get_unifi_api",
|
||||
side_effect=Exception,
|
||||
):
|
||||
await setup_unifi_integration(hass)
|
||||
assert hass.data[UNIFI_DOMAIN] == {}
|
||||
|
||||
|
||||
async def test_config_entry_updated(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Calling reset when the entry has been setup."""
|
||||
config_entry = await setup_unifi_integration(hass, aioclient_mock)
|
||||
hub = config_entry.runtime_data
|
||||
|
||||
event_call = Mock()
|
||||
unsub = async_dispatcher_connect(hass, hub.signal_options_update, event_call)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.options[CONF_TRACK_CLIENTS] is False
|
||||
assert config_entry.options[CONF_TRACK_DEVICES] is False
|
||||
|
||||
event_call.assert_called_once()
|
||||
|
||||
unsub()
|
||||
|
||||
|
||||
async def test_reset_after_successful_setup(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
hass: HomeAssistant, setup_config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Calling reset when the entry has been setup."""
|
||||
config_entry = await setup_unifi_integration(hass, aioclient_mock)
|
||||
hub = config_entry.runtime_data
|
||||
config_entry = setup_config_entry
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
result = await hub.async_reset()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result is True
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_reset_fails(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
hass: HomeAssistant, setup_config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Calling reset when the entry has been setup can return false."""
|
||||
config_entry = await setup_unifi_integration(hass, aioclient_mock)
|
||||
hub = config_entry.runtime_data
|
||||
config_entry = setup_config_entry
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_forward_entry_unload",
|
||||
return_value=False,
|
||||
):
|
||||
result = await hub.async_reset()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result is False
|
||||
assert not await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"hostname": "client",
|
||||
"ip": "10.0.0.1",
|
||||
"is_wired": True,
|
||||
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
|
||||
"mac": "00:00:00:00:00:01",
|
||||
},
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_connection_state_signalling(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
mock_device_registry,
|
||||
setup_config_entry: ConfigEntry,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Verify connection statesignalling and connection state are working."""
|
||||
client = {
|
||||
"hostname": "client",
|
||||
"ip": "10.0.0.1",
|
||||
"is_wired": True,
|
||||
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
|
||||
"mac": "00:00:00:00:00:01",
|
||||
}
|
||||
await setup_unifi_integration(hass, aioclient_mock, clients_response=[client])
|
||||
|
||||
# Controller is connected
|
||||
assert hass.states.get("device_tracker.client").state == "home"
|
||||
|
||||
@@ -407,11 +347,12 @@ async def test_connection_state_signalling(
|
||||
|
||||
|
||||
async def test_reconnect_mechanism(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, websocket_mock
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_config_entry: ConfigEntry,
|
||||
websocket_mock,
|
||||
) -> None:
|
||||
"""Verify reconnect prints only on first reconnection try."""
|
||||
await setup_unifi_integration(hass, aioclient_mock)
|
||||
|
||||
aioclient_mock.clear_requests()
|
||||
aioclient_mock.get(f"https://{DEFAULT_HOST}:1234/", status=HTTPStatus.BAD_GATEWAY)
|
||||
|
||||
@@ -435,11 +376,13 @@ async def test_reconnect_mechanism(
|
||||
],
|
||||
)
|
||||
async def test_reconnect_mechanism_exceptions(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, websocket_mock, exception
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_config_entry: ConfigEntry,
|
||||
websocket_mock,
|
||||
exception,
|
||||
) -> None:
|
||||
"""Verify async_reconnect calls expected methods."""
|
||||
await setup_unifi_integration(hass, aioclient_mock)
|
||||
|
||||
with (
|
||||
patch("aiounifi.Controller.login", side_effect=exception),
|
||||
patch(
|
||||
@@ -452,20 +395,6 @@ async def test_reconnect_mechanism_exceptions(
|
||||
mock_reconnect.assert_called_once()
|
||||
|
||||
|
||||
async def test_get_unifi_api(hass: HomeAssistant) -> None:
|
||||
"""Successful call."""
|
||||
with patch("aiounifi.Controller.login", return_value=True):
|
||||
assert await get_unifi_api(hass, ENTRY_CONFIG)
|
||||
|
||||
|
||||
async def test_get_unifi_api_verify_ssl_false(hass: HomeAssistant) -> None:
|
||||
"""Successful call with verify ssl set to false."""
|
||||
hub_data = dict(ENTRY_CONFIG)
|
||||
hub_data[CONF_VERIFY_SSL] = False
|
||||
with patch("aiounifi.Controller.login", return_value=True):
|
||||
assert await get_unifi_api(hass, hub_data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "raised_exception"),
|
||||
[
|
||||
|
@@ -1,9 +1,11 @@
|
||||
"""Test UniFi Network integration setup process."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiounifi.models.message import MessageKey
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import unifi
|
||||
from homeassistant.components.unifi.const import (
|
||||
@@ -14,11 +16,12 @@ from homeassistant.components.unifi.const import (
|
||||
DOMAIN as UNIFI_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_hub import DEFAULT_CONFIG_ENTRY_ID, setup_unifi_integration
|
||||
from .test_hub import DEFAULT_CONFIG_ENTRY_ID
|
||||
|
||||
from tests.common import flush_store
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
@@ -31,18 +34,22 @@ async def test_setup_with_no_config(hass: HomeAssistant) -> None:
|
||||
assert UNIFI_DOMAIN not in hass.data
|
||||
|
||||
|
||||
async def test_setup_entry_fails_config_entry_not_ready(hass: HomeAssistant) -> None:
|
||||
async def test_setup_entry_fails_config_entry_not_ready(
|
||||
hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry]
|
||||
) -> None:
|
||||
"""Failed authentication trigger a reauthentication flow."""
|
||||
with patch(
|
||||
"homeassistant.components.unifi.get_unifi_api",
|
||||
side_effect=CannotConnect,
|
||||
):
|
||||
await setup_unifi_integration(hass)
|
||||
config_entry = await prepare_config_entry()
|
||||
|
||||
assert hass.data[UNIFI_DOMAIN] == {}
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_entry_fails_trigger_reauth_flow(hass: HomeAssistant) -> None:
|
||||
async def test_setup_entry_fails_trigger_reauth_flow(
|
||||
hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry]
|
||||
) -> None:
|
||||
"""Failed authentication trigger a reauthentication flow."""
|
||||
with (
|
||||
patch(
|
||||
@@ -51,16 +58,35 @@ async def test_setup_entry_fails_trigger_reauth_flow(hass: HomeAssistant) -> Non
|
||||
),
|
||||
patch.object(hass.config_entries.flow, "async_init") as mock_flow_init,
|
||||
):
|
||||
await setup_unifi_integration(hass)
|
||||
config_entry = await prepare_config_entry()
|
||||
mock_flow_init.assert_called_once()
|
||||
|
||||
assert hass.data[UNIFI_DOMAIN] == {}
|
||||
assert config_entry.state == ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"hostname": "client_1",
|
||||
"ip": "10.0.0.1",
|
||||
"is_wired": False,
|
||||
"mac": "00:00:00:00:00:01",
|
||||
},
|
||||
{
|
||||
"hostname": "client_2",
|
||||
"ip": "10.0.0.2",
|
||||
"is_wired": False,
|
||||
"mac": "00:00:00:00:00:02",
|
||||
},
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_wireless_clients(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
prepare_config_entry: Callable[[], ConfigEntry],
|
||||
) -> None:
|
||||
"""Verify wireless clients class."""
|
||||
hass_storage[unifi.STORAGE_KEY] = {
|
||||
@@ -72,21 +98,7 @@ async def test_wireless_clients(
|
||||
},
|
||||
}
|
||||
|
||||
client_1 = {
|
||||
"hostname": "client_1",
|
||||
"ip": "10.0.0.1",
|
||||
"is_wired": False,
|
||||
"mac": "00:00:00:00:00:01",
|
||||
}
|
||||
client_2 = {
|
||||
"hostname": "client_2",
|
||||
"ip": "10.0.0.2",
|
||||
"is_wired": False,
|
||||
"mac": "00:00:00:00:00:02",
|
||||
}
|
||||
await setup_unifi_integration(
|
||||
hass, aioclient_mock, clients_response=[client_1, client_2]
|
||||
)
|
||||
await prepare_config_entry()
|
||||
await flush_store(hass.data[unifi.UNIFI_WIRELESS_CLIENTS]._store)
|
||||
|
||||
assert sorted(hass_storage[unifi.STORAGE_KEY]["data"]["wireless_clients"]) == [
|
||||
@@ -96,98 +108,113 @@ async def test_wireless_clients(
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"hostname": "Wired client",
|
||||
"is_wired": True,
|
||||
"mac": "00:00:00:00:00:01",
|
||||
"oui": "Producer",
|
||||
"wired-rx_bytes": 1234000000,
|
||||
"wired-tx_bytes": 5678000000,
|
||||
"uptime": 1600094505,
|
||||
},
|
||||
{
|
||||
"is_wired": False,
|
||||
"mac": "00:00:00:00:00:02",
|
||||
"name": "Wireless client",
|
||||
"oui": "Producer",
|
||||
"rx_bytes": 2345000000,
|
||||
"tx_bytes": 6789000000,
|
||||
"uptime": 60,
|
||||
},
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"device_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"board_rev": 3,
|
||||
"device_id": "mock-id",
|
||||
"has_fan": True,
|
||||
"fan_level": 0,
|
||||
"ip": "10.0.1.1",
|
||||
"last_seen": 1562600145,
|
||||
"mac": "00:00:00:00:01:01",
|
||||
"model": "US16P150",
|
||||
"name": "Device 1",
|
||||
"next_interval": 20,
|
||||
"overheating": True,
|
||||
"state": 1,
|
||||
"type": "usw",
|
||||
"upgradable": True,
|
||||
"version": "4.0.42.10433",
|
||||
}
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"config_entry_options",
|
||||
[
|
||||
{
|
||||
CONF_ALLOW_BANDWIDTH_SENSORS: True,
|
||||
CONF_ALLOW_UPTIME_SENSORS: True,
|
||||
CONF_TRACK_CLIENTS: True,
|
||||
CONF_TRACK_DEVICES: True,
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_remove_config_entry_device(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
prepare_config_entry: Callable[[], ConfigEntry],
|
||||
client_payload: list[dict[str, Any]],
|
||||
device_payload: list[dict[str, Any]],
|
||||
mock_unifi_websocket,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Verify removing a device manually."""
|
||||
client_1 = {
|
||||
"hostname": "Wired client",
|
||||
"is_wired": True,
|
||||
"mac": "00:00:00:00:00:01",
|
||||
"oui": "Producer",
|
||||
"wired-rx_bytes": 1234000000,
|
||||
"wired-tx_bytes": 5678000000,
|
||||
"uptime": 1600094505,
|
||||
}
|
||||
client_2 = {
|
||||
"is_wired": False,
|
||||
"mac": "00:00:00:00:00:02",
|
||||
"name": "Wireless client",
|
||||
"oui": "Producer",
|
||||
"rx_bytes": 2345000000,
|
||||
"tx_bytes": 6789000000,
|
||||
"uptime": 60,
|
||||
}
|
||||
device_1 = {
|
||||
"board_rev": 3,
|
||||
"device_id": "mock-id",
|
||||
"has_fan": True,
|
||||
"fan_level": 0,
|
||||
"ip": "10.0.1.1",
|
||||
"last_seen": 1562600145,
|
||||
"mac": "00:00:00:00:01:01",
|
||||
"model": "US16P150",
|
||||
"name": "Device 1",
|
||||
"next_interval": 20,
|
||||
"overheating": True,
|
||||
"state": 1,
|
||||
"type": "usw",
|
||||
"upgradable": True,
|
||||
"version": "4.0.42.10433",
|
||||
}
|
||||
options = {
|
||||
CONF_ALLOW_BANDWIDTH_SENSORS: True,
|
||||
CONF_ALLOW_UPTIME_SENSORS: True,
|
||||
CONF_TRACK_CLIENTS: True,
|
||||
CONF_TRACK_DEVICES: True,
|
||||
}
|
||||
|
||||
config_entry = await setup_unifi_integration(
|
||||
hass,
|
||||
aioclient_mock,
|
||||
options=options,
|
||||
clients_response=[client_1, client_2],
|
||||
devices_response=[device_1],
|
||||
)
|
||||
config_entry = await prepare_config_entry()
|
||||
|
||||
assert await async_setup_component(hass, "config", {})
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
# Try to remove an active client from UI: not allowed
|
||||
device_entry = device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_1["mac"])}
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}
|
||||
)
|
||||
response = await ws_client.remove_device(device_entry.id, config_entry.entry_id)
|
||||
assert not response["success"]
|
||||
assert device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_1["mac"])}
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_payload[0]["mac"])}
|
||||
)
|
||||
|
||||
# Try to remove an active device from UI: not allowed
|
||||
device_entry = device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_1["mac"])}
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_payload[0]["mac"])}
|
||||
)
|
||||
response = await ws_client.remove_device(device_entry.id, config_entry.entry_id)
|
||||
assert not response["success"]
|
||||
assert device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_1["mac"])}
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_payload[0]["mac"])}
|
||||
)
|
||||
|
||||
# Remove a client from Unifi API
|
||||
mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[client_2])
|
||||
mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[client_payload[1]])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Try to remove an inactive client from UI: allowed
|
||||
device_entry = device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_2["mac"])}
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_payload[1]["mac"])}
|
||||
)
|
||||
response = await ws_client.remove_device(device_entry.id, config_entry.entry_id)
|
||||
assert response["success"]
|
||||
assert not device_registry.async_get_device(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_2["mac"])}
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, client_payload[1]["mac"])}
|
||||
)
|
||||
|
@@ -1023,7 +1023,7 @@ async def _mqtt_mock_entry(
|
||||
mock_mqtt_instance.connected = True
|
||||
mqtt_client_mock.on_connect(mqtt_client_mock, None, 0, 0, 0)
|
||||
|
||||
async_dispatcher_send(hass, mqtt.MQTT_CONNECTED)
|
||||
async_dispatcher_send(hass, mqtt.MQTT_CONNECTION_STATE, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_mqtt_instance
|
||||
|
Reference in New Issue
Block a user