Compare commits

..

1 Commits

Author SHA1 Message Date
Petar Petrov
03b58a4c21 Support for hierarchy of water meters 2025-10-07 14:09:45 +03:00
41 changed files with 316 additions and 1028 deletions

View File

@@ -16,12 +16,10 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfSoundPressure,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
@@ -114,21 +112,6 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"lux": SensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"noise": SensorEntityDescription(
key="noise",
translation_key="ambient_noise",
device_class=SensorDeviceClass.SOUND_PRESSURE,
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
}
PARALLEL_UPDATES = 0

View File

@@ -41,9 +41,6 @@
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
},
"ambient_noise": {
"name": "Ambient noise"
}
}
}

View File

@@ -38,11 +38,9 @@ from home_assistant_intents import (
ErrorKey,
FuzzyConfig,
FuzzyLanguageResponses,
LanguageScores,
get_fuzzy_config,
get_fuzzy_language,
get_intents,
get_language_scores,
get_languages,
)
import yaml
@@ -61,7 +59,6 @@ from homeassistant.core import (
)
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
device_registry as dr,
entity_registry as er,
floor_registry as fr,
@@ -346,81 +343,6 @@ class DefaultAgent(ConversationEntity):
return result
async def async_debug_recognize(
self, user_input: ConversationInput
) -> dict[str, Any] | None:
"""Debug recognize from user input."""
result_dict: dict[str, Any] | None = None
if trigger_result := await self.async_recognize_sentence_trigger(user_input):
result_dict = {
# Matched a user-defined sentence trigger.
# We can't provide the response here without executing the
# trigger.
"match": True,
"source": "trigger",
"sentence_template": trigger_result.sentence_template or "",
}
elif intent_result := await self.async_recognize_intent(user_input):
successful_match = not intent_result.unmatched_entities
result_dict = {
# Name of the matching intent (or the closest)
"intent": {
"name": intent_result.intent.name,
},
# Slot values that would be received by the intent
"slots": { # direct access to values
entity_key: entity.text or entity.value
for entity_key, entity in intent_result.entities.items()
},
# Extra slot details, such as the originally matched text
"details": {
entity_key: {
"name": entity.name,
"value": entity.value,
"text": entity.text,
}
for entity_key, entity in intent_result.entities.items()
},
# Entities/areas/etc. that would be targeted
"targets": {},
# True if match was successful
"match": successful_match,
# Text of the sentence template that matched (or was closest)
"sentence_template": "",
# When match is incomplete, this will contain the best slot guesses
"unmatched_slots": _get_unmatched_slots(intent_result),
# True if match was not exact
"fuzzy_match": False,
}
if successful_match:
result_dict["targets"] = {
state.entity_id: {"matched": is_matched}
for state, is_matched in _get_debug_targets(
self.hass, intent_result
)
}
if intent_result.intent_sentence is not None:
result_dict["sentence_template"] = intent_result.intent_sentence.text
if intent_result.intent_metadata:
# Inspect metadata to determine if this matched a custom sentence
if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE):
result_dict["source"] = "custom"
result_dict["file"] = intent_result.intent_metadata.get(
METADATA_CUSTOM_FILE
)
else:
result_dict["source"] = "builtin"
result_dict["fuzzy_match"] = intent_result.intent_metadata.get(
METADATA_FUZZY_MATCH, False
)
return result_dict
async def _async_handle_message(
self,
user_input: ConversationInput,
@@ -1607,10 +1529,6 @@ class DefaultAgent(ConversationEntity):
return None
return response
async def async_get_language_scores(self) -> dict[str, LanguageScores]:
"""Get support scores per language."""
return await self.hass.async_add_executor_job(get_language_scores)
def _make_error_result(
language: str,
@@ -1807,75 +1725,3 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No
elif isinstance(expression, ListReference):
# {list}
list_names.add(expression.slot_name)
def _get_debug_targets(
hass: HomeAssistant,
result: RecognizeResult,
) -> Iterable[tuple[State, bool]]:
"""Yield state/is_matched pairs for a hassil recognition."""
entities = result.entities
name: str | None = None
area_name: str | None = None
domains: set[str] | None = None
device_classes: set[str] | None = None
state_names: set[str] | None = None
if "name" in entities:
name = str(entities["name"].value)
if "area" in entities:
area_name = str(entities["area"].value)
if "domain" in entities:
domains = set(cv.ensure_list(entities["domain"].value))
if "device_class" in entities:
device_classes = set(cv.ensure_list(entities["device_class"].value))
if "state" in entities:
# HassGetState only
state_names = set(cv.ensure_list(entities["state"].value))
if (
(name is None)
and (area_name is None)
and (not domains)
and (not device_classes)
and (not state_names)
):
# Avoid "matching" all entities when there is no filter
return
states = intent.async_match_states(
hass,
name=name,
area_name=area_name,
domains=domains,
device_classes=device_classes,
)
for state in states:
# For queries, a target is "matched" based on its state
is_matched = (state_names is None) or (state.state in state_names)
yield state, is_matched
def _get_unmatched_slots(
result: RecognizeResult,
) -> dict[str, str | int | float]:
"""Return a dict of unmatched text/range slot entities."""
unmatched_slots: dict[str, str | int | float] = {}
for entity in result.unmatched_entities_list:
if isinstance(entity, UnmatchedTextEntity):
if entity.text == MISSING_ENTITY:
# Don't report <missing> since these are just missing context
# slots.
continue
unmatched_slots[entity.name] = entity.text
elif isinstance(entity, UnmatchedRangeEntity):
unmatched_slots[entity.name] = entity.value
return unmatched_slots

View File

@@ -2,16 +2,21 @@
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import asdict
from typing import Any
from aiohttp import web
from hassil.recognize import MISSING_ENTITY, RecognizeResult
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from home_assistant_intents import get_language_scores
import voluptuous as vol
from homeassistant.components import http, websocket_api
from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.const import MATCH_ALL
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, intent
from homeassistant.util import language as language_util
from .agent_manager import (
@@ -21,6 +26,11 @@ from .agent_manager import (
get_agent_manager,
)
from .const import DATA_COMPONENT
from .default_agent import (
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
METADATA_FUZZY_MATCH,
)
from .entity import ConversationEntity
from .models import ConversationInput
@@ -196,12 +206,150 @@ async def websocket_hass_agent_debug(
language=msg.get("language", hass.config.language),
agent_id=agent.entity_id,
)
result_dict = await agent.async_debug_recognize(user_input)
result_dict: dict[str, Any] | None = None
if trigger_result := await agent.async_recognize_sentence_trigger(user_input):
result_dict = {
# Matched a user-defined sentence trigger.
# We can't provide the response here without executing the
# trigger.
"match": True,
"source": "trigger",
"sentence_template": trigger_result.sentence_template or "",
}
elif intent_result := await agent.async_recognize_intent(user_input):
successful_match = not intent_result.unmatched_entities
result_dict = {
# Name of the matching intent (or the closest)
"intent": {
"name": intent_result.intent.name,
},
# Slot values that would be received by the intent
"slots": { # direct access to values
entity_key: entity.text or entity.value
for entity_key, entity in intent_result.entities.items()
},
# Extra slot details, such as the originally matched text
"details": {
entity_key: {
"name": entity.name,
"value": entity.value,
"text": entity.text,
}
for entity_key, entity in intent_result.entities.items()
},
# Entities/areas/etc. that would be targeted
"targets": {},
# True if match was successful
"match": successful_match,
# Text of the sentence template that matched (or was closest)
"sentence_template": "",
# When match is incomplete, this will contain the best slot guesses
"unmatched_slots": _get_unmatched_slots(intent_result),
# True if match was not exact
"fuzzy_match": False,
}
if successful_match:
result_dict["targets"] = {
state.entity_id: {"matched": is_matched}
for state, is_matched in _get_debug_targets(hass, intent_result)
}
if intent_result.intent_sentence is not None:
result_dict["sentence_template"] = intent_result.intent_sentence.text
if intent_result.intent_metadata:
# Inspect metadata to determine if this matched a custom sentence
if intent_result.intent_metadata.get(METADATA_CUSTOM_SENTENCE):
result_dict["source"] = "custom"
result_dict["file"] = intent_result.intent_metadata.get(
METADATA_CUSTOM_FILE
)
else:
result_dict["source"] = "builtin"
result_dict["fuzzy_match"] = intent_result.intent_metadata.get(
METADATA_FUZZY_MATCH, False
)
result_dicts.append(result_dict)
connection.send_result(msg["id"], {"results": result_dicts})
def _get_debug_targets(
hass: HomeAssistant,
result: RecognizeResult,
) -> Iterable[tuple[State, bool]]:
"""Yield state/is_matched pairs for a hassil recognition."""
entities = result.entities
name: str | None = None
area_name: str | None = None
domains: set[str] | None = None
device_classes: set[str] | None = None
state_names: set[str] | None = None
if "name" in entities:
name = str(entities["name"].value)
if "area" in entities:
area_name = str(entities["area"].value)
if "domain" in entities:
domains = set(cv.ensure_list(entities["domain"].value))
if "device_class" in entities:
device_classes = set(cv.ensure_list(entities["device_class"].value))
if "state" in entities:
# HassGetState only
state_names = set(cv.ensure_list(entities["state"].value))
if (
(name is None)
and (area_name is None)
and (not domains)
and (not device_classes)
and (not state_names)
):
# Avoid "matching" all entities when there is no filter
return
states = intent.async_match_states(
hass,
name=name,
area_name=area_name,
domains=domains,
device_classes=device_classes,
)
for state in states:
# For queries, a target is "matched" based on its state
is_matched = (state_names is None) or (state.state in state_names)
yield state, is_matched
def _get_unmatched_slots(
result: RecognizeResult,
) -> dict[str, str | int | float]:
"""Return a dict of unmatched text/range slot entities."""
unmatched_slots: dict[str, str | int | float] = {}
for entity in result.unmatched_entities_list:
if isinstance(entity, UnmatchedTextEntity):
if entity.text == MISSING_ENTITY:
# Don't report <missing> since these are just missing context
# slots.
continue
unmatched_slots[entity.name] = entity.text
elif isinstance(entity, UnmatchedRangeEntity):
unmatched_slots[entity.name] = entity.value
return unmatched_slots
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/agent/homeassistant/language_scores",
@@ -216,13 +364,10 @@ async def websocket_hass_agent_language_scores(
msg: dict[str, Any],
) -> None:
"""Get support scores per language."""
agent = get_agent_manager(hass).default_agent
assert agent is not None
language = msg.get("language", hass.config.language)
country = msg.get("country", hass.config.country)
scores = await agent.async_get_language_scores()
scores = await hass.async_add_executor_job(get_language_scores)
matching_langs = language_util.matches(language, scores.keys(), country=country)
preferred_lang = matching_langs[0] if matching_langs else language
result = {

View File

@@ -116,6 +116,10 @@ class WaterSourceType(TypedDict):
# an EnergyCostSensor will be automatically created
stat_cost: str | None
# An optional statistic_id identifying a device
# that includes this device's consumption in its total
included_in_stat: str | None
# Used to generate costs if stat_cost is set to None
entity_energy_price: str | None # entity_id of an entity providing price ($/m³)
number_energy_price: float | None # Price for energy ($/m³)

View File

@@ -68,7 +68,6 @@ EVENT_HEALTH_CHANGED = "health_changed"
EVENT_SUPPORTED_CHANGED = "supported_changed"
EVENT_ISSUE_CHANGED = "issue_changed"
EVENT_ISSUE_REMOVED = "issue_removed"
EVENT_JOB = "job"
UPDATE_KEY_SUPERVISOR = "supervisor"

View File

@@ -56,7 +56,6 @@ from .const import (
SupervisorEntityModel,
)
from .handler import HassioAPIError, get_supervisor_client
from .jobs import SupervisorJobs
if TYPE_CHECKING:
from .issues import SupervisorIssues
@@ -312,7 +311,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
lambda: defaultdict(set)
)
self.supervisor_client = get_supervisor_client(hass)
self.jobs = SupervisorJobs(hass)
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
@@ -487,9 +485,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
)
)
# Refresh jobs data
await self.jobs.refresh_data(first_update)
async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Update single addon stats."""
try:

View File

@@ -1,157 +0,0 @@
"""Track Supervisor job data and allow subscription to updates."""
from collections.abc import Callable
from dataclasses import dataclass, replace
from functools import partial
from typing import Any
from uuid import UUID
from aiohasupervisor.models import Job
from homeassistant.core import (
CALLBACK_TYPE,
HomeAssistant,
callback,
is_callback_check_partial,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
ATTR_DATA,
ATTR_UPDATE_KEY,
ATTR_WS_EVENT,
EVENT_JOB,
EVENT_SUPERVISOR_EVENT,
EVENT_SUPERVISOR_UPDATE,
UPDATE_KEY_SUPERVISOR,
)
from .handler import get_supervisor_client
@dataclass(slots=True, frozen=True)
class JobSubscription:
"""Subscribe for updates on jobs which match filters.
UUID is preferred match but only available in cases of a background API that
returns the UUID before taking the action. Others are used to match jobs only
if UUID is omitted. Either name or UUID is required to be able to match.
event_callback must be safe annotated as a homeassistant.core.callback
and safe to call in the event loop.
"""
event_callback: Callable[[Job], Any]
uuid: str | None = None
name: str | None = None
reference: str | None | type[Any] = Any
def __post_init__(self) -> None:
"""Validate at least one filter option is present."""
if not self.name and not self.uuid:
raise ValueError("Either name or uuid must be provided!")
if not is_callback_check_partial(self.event_callback):
raise ValueError("event_callback must be a homeassistant.core.callback!")
def matches(self, job: Job) -> bool:
"""Return true if job matches subscription filters."""
if self.uuid:
return job.uuid == self.uuid
return job.name == self.name and self.reference in (Any, job.reference)
class SupervisorJobs:
"""Manage access to Supervisor jobs."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize object."""
self._hass = hass
self._supervisor_client = get_supervisor_client(hass)
self._jobs: dict[UUID, Job] = {}
self._subscriptions: set[JobSubscription] = set()
@property
def current_jobs(self) -> list[Job]:
"""Return current jobs."""
return list(self._jobs.values())
def subscribe(self, subscription: JobSubscription) -> CALLBACK_TYPE:
"""Subscribe to updates for job. Return callback is used to unsubscribe.
If any jobs match the subscription at the time this is called, creates
tasks to run their callback on it.
"""
self._subscriptions.add(subscription)
# As these are callbacks they are safe to run in the event loop
# We wrap these in an asyncio task so subscribing does not wait on the logic
if matches := [job for job in self._jobs.values() if subscription.matches(job)]:
async def event_callback_async(job: Job) -> Any:
return subscription.event_callback(job)
for match in matches:
self._hass.async_create_task(event_callback_async(match))
return partial(self._subscriptions.discard, subscription)
async def refresh_data(self, first_update: bool = False) -> None:
"""Refresh job data."""
job_data = await self._supervisor_client.jobs.info()
job_queue: list[Job] = job_data.jobs.copy()
new_jobs: dict[UUID, Job] = {}
changed_jobs: list[Job] = []
# Rebuild our job cache from new info and compare to find changes
while job_queue:
job = job_queue.pop(0)
job_queue.extend(job.child_jobs)
job = replace(job, child_jobs=[])
if job.uuid not in self._jobs or job != self._jobs[job.uuid]:
changed_jobs.append(job)
new_jobs[job.uuid] = replace(job, child_jobs=[])
# For any jobs that disappeared which weren't done, tell subscribers they
# changed to done. We don't know what else happened to them so leave the
# rest of their state as is rather then guessing
changed_jobs.extend(
[
replace(job, done=True)
for uuid, job in self._jobs.items()
if uuid not in new_jobs and job.done is False
]
)
# Replace our cache and inform subscribers of all changes
self._jobs = new_jobs
for job in changed_jobs:
self._process_job_change(job)
# If this is the first update register to receive Supervisor events
if first_update:
async_dispatcher_connect(
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_jobs
)
@callback
def _supervisor_events_to_jobs(self, event: dict[str, Any]) -> None:
"""Update job data cache from supervisor events."""
if ATTR_WS_EVENT not in event:
return
if (
event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE
and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR
):
self._hass.async_create_task(self.refresh_data())
elif event[ATTR_WS_EVENT] == EVENT_JOB:
job = Job.from_dict(event[ATTR_DATA] | {"child_jobs": []})
self._jobs[job.uuid] = job
self._process_job_change(job)
def _process_job_change(self, job: Job) -> None:
"""Process a job change by triggering callbacks on subscribers."""
for sub in self._subscriptions:
if sub.matches(job):
sub.event_callback(job)

View File

@@ -6,7 +6,6 @@ import re
from typing import Any
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import Job
from awesomeversion import AwesomeVersion, AwesomeVersionStrategy
from homeassistant.components.update import (
@@ -16,7 +15,7 @@ from homeassistant.components.update import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ICON, ATTR_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -36,7 +35,6 @@ from .entity import (
HassioOSEntity,
HassioSupervisorEntity,
)
from .jobs import JobSubscription
from .update_helper import update_addon, update_core, update_os
ENTITY_DESCRIPTION = UpdateEntityDescription(
@@ -91,7 +89,6 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.BACKUP
| UpdateEntityFeature.RELEASE_NOTES
| UpdateEntityFeature.PROGRESS
)
@property
@@ -157,30 +154,6 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
)
await self.coordinator.async_refresh()
@callback
def _update_job_changed(self, job: Job) -> None:
"""Process update for this entity's update job."""
if job.done is False:
self._attr_in_progress = True
self._attr_update_percentage = job.progress
else:
self._attr_in_progress = False
self._attr_update_percentage = None
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Subscribe to progress updates."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.jobs.subscribe(
JobSubscription(
self._update_job_changed,
name="addon_manager_update",
reference=self._addon_slug,
)
)
)
class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
"""Update entity to handle updates for the Home Assistant Operating System."""
@@ -277,7 +250,6 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.SPECIFIC_VERSION
| UpdateEntityFeature.BACKUP
| UpdateEntityFeature.PROGRESS
)
_attr_title = "Home Assistant Core"
@@ -309,25 +281,3 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
) -> None:
"""Install an update."""
await update_core(self.hass, version, backup)
@callback
def _update_job_changed(self, job: Job) -> None:
"""Process update for this entity's update job."""
if job.done is False:
self._attr_in_progress = True
self._attr_update_percentage = job.progress
else:
self._attr_in_progress = False
self._attr_update_percentage = None
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Subscribe to progress updates."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator.jobs.subscribe(
JobSubscription(
self._update_job_changed, name="home_assistant_core_update"
)
)
)

View File

@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.1.2"]
"requirements": ["pylamarzocco==2.1.1"]
}

View File

@@ -174,7 +174,7 @@ class MotionBaseDevice(MotionCoordinatorEntity, CoverEntity):
_restore_tilt = False
def __init__(self, coordinator, blind, device_class) -> None:
def __init__(self, coordinator, blind, device_class):
"""Initialize the blind."""
super().__init__(coordinator, blind)
@@ -275,7 +275,7 @@ class MotionTiltDevice(MotionPositionDevice):
"""
if self._blind.angle is None:
return None
return 100 - (self._blind.angle * 100 / 180)
return self._blind.angle * 100 / 180
@property
def is_closed(self) -> bool | None:
@@ -287,14 +287,14 @@ class MotionTiltDevice(MotionPositionDevice):
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)
await self.async_request_position_till_stop()
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)
await self.async_request_position_till_stop()
@@ -302,7 +302,7 @@ class MotionTiltDevice(MotionPositionDevice):
"""Move the cover tilt to a specific position."""
angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 180 - angle)
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
await self.async_request_position_till_stop()
@@ -347,9 +347,9 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
if self._blind.position is None:
if self._blind.angle is None:
return None
return 100 - (self._blind.angle * 100 / 180)
return self._blind.angle * 100 / 180
return 100 - self._blind.position
return self._blind.position
@property
def is_closed(self) -> bool | None:
@@ -357,9 +357,9 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
if self._blind.position is None:
if self._blind.angle is None:
return None
return self._blind.angle == 180
return self._blind.angle == 0
return self._blind.position == 100
return self._blind.position == 0
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
@@ -381,14 +381,10 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
if self._blind.position is None:
angle = angle * 180 / 100
async with self._api_lock:
await self.hass.async_add_executor_job(
self._blind.Set_angle, 180 - angle
)
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
else:
async with self._api_lock:
await self.hass.async_add_executor_job(
self._blind.Set_position, 100 - angle
)
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
await self.async_request_position_till_stop()
@@ -401,14 +397,10 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
if self._blind.position is None:
angle = angle * 180 / 100
async with self._api_lock:
await self.hass.async_add_executor_job(
self._blind.Set_angle, 180 - angle
)
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)
else:
async with self._api_lock:
await self.hass.async_add_executor_job(
self._blind.Set_position, 100 - angle
)
await self.hass.async_add_executor_job(self._blind.Set_position, angle)
await self.async_request_position_till_stop()
@@ -416,7 +408,7 @@ class MotionTiltOnlyDevice(MotionTiltDevice):
class MotionTDBUDevice(MotionBaseDevice):
"""Representation of a Motion Top Down Bottom Up blind Device."""
def __init__(self, coordinator, blind, device_class, motor) -> None:
def __init__(self, coordinator, blind, device_class, motor):
"""Initialize the blind."""
super().__init__(coordinator, blind, device_class)
self._motor = motor

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoparental==1.1.1"]
"requirements": ["pynintendoparental==1.0.1"]
}

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["openai==2.2.0", "python-open-router==0.3.1"]
"requirements": ["openai==1.99.5", "python-open-router==0.3.1"]
}

View File

@@ -487,7 +487,7 @@ class OpenAIBaseLLMEntity(Entity):
if options.get(CONF_WEB_SEARCH):
web_search = WebSearchToolParam(
type="web_search",
type="web_search_preview",
search_context_size=options.get(
CONF_WEB_SEARCH_CONTEXT_SIZE, RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE
),

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["openai==2.2.0"]
"requirements": ["openai==1.99.5"]
}

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, cast
from typing import Any, cast
from aioshelly.block_device import Block
from aioshelly.const import RPC_GENERATIONS
@@ -37,7 +37,6 @@ from .entity import (
ShellySleepingBlockAttributeEntity,
async_setup_entry_attribute_entities,
async_setup_entry_rpc,
rpc_call,
)
from .utils import (
async_remove_orphaned_entities,
@@ -79,7 +78,7 @@ class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription):
is_on: Callable[[dict[str, Any]], bool]
method_on: str
method_off: str
method_params_fn: Callable[[int | None, bool], tuple]
method_params_fn: Callable[[int | None, bool], dict]
RPC_RELAY_SWITCHES = {
@@ -88,9 +87,9 @@ RPC_RELAY_SWITCHES = {
sub_key="output",
removal_condition=is_rpc_exclude_from_relay,
is_on=lambda status: bool(status["output"]),
method_on="switch_set",
method_off="switch_set",
method_params_fn=lambda id, value: (id, value),
method_on="Switch.Set",
method_off="Switch.Set",
method_params_fn=lambda id, value: {"id": id, "on": value},
),
}
@@ -102,9 +101,9 @@ RPC_SWITCHES = {
config, key, SWITCH_PLATFORM
),
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="generic",
),
"boolean_anti_freeze": RpcSwitchDescription(
@@ -112,9 +111,9 @@ RPC_SWITCHES = {
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="anti_freeze",
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
@@ -122,9 +121,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="child_lock",
models={MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
@@ -133,9 +132,9 @@ RPC_SWITCHES = {
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="enable",
models={MODEL_LINKEDGO_ST802_THERMOSTAT, MODEL_LINKEDGO_ST1820_THERMOSTAT},
),
@@ -143,9 +142,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="start_charging",
models={MODEL_TOP_EV_CHARGER_EVE01},
),
@@ -154,9 +153,9 @@ RPC_SWITCHES = {
sub_key="value",
entity_registry_enabled_default=False,
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="state",
models={MODEL_NEO_WATER_VALVE},
),
@@ -164,9 +163,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone0",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
@@ -174,9 +173,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone1",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
@@ -184,9 +183,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone2",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
@@ -194,9 +193,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone3",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
@@ -204,9 +203,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone4",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
@@ -214,9 +213,9 @@ RPC_SWITCHES = {
key="boolean",
sub_key="value",
is_on=lambda status: bool(status["value"]),
method_on="boolean_set",
method_off="boolean_set",
method_params_fn=lambda id, value: (id, value),
method_on="Boolean.Set",
method_off="Boolean.Set",
method_params_fn=lambda id, value: {"id": id, "value": value},
role="zone5",
models={MODEL_FRANKEVER_IRRIGATION_CONTROLLER},
),
@@ -224,9 +223,9 @@ RPC_SWITCHES = {
key="script",
sub_key="running",
is_on=lambda status: bool(status["running"]),
method_on="script_start",
method_off="script_stop",
method_params_fn=lambda id, _: (id,),
method_on="Script.Start",
method_off="Script.Stop",
method_params_fn=lambda id, _: {"id": id},
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
),
@@ -423,27 +422,19 @@ class RpcSwitch(ShellyRpcAttributeEntity, SwitchEntity):
"""If switch is on."""
return self.entity_description.is_on(self.status)
@rpc_call
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on switch."""
method = getattr(self.coordinator.device, self.entity_description.method_on)
"""Turn on relay."""
await self.call_rpc(
self.entity_description.method_on,
self.entity_description.method_params_fn(self._id, True),
)
if TYPE_CHECKING:
assert method is not None
params = self.entity_description.method_params_fn(self._id, True)
await method(*params)
@rpc_call
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off switch."""
method = getattr(self.coordinator.device, self.entity_description.method_off)
if TYPE_CHECKING:
assert method is not None
params = self.entity_description.method_params_fn(self._id, False)
await method(*params)
"""Turn off relay."""
await self.call_rpc(
self.entity_description.method_off,
self.entity_description.method_params_fn(self._id, False),
)
class RpcRelaySwitch(RpcSwitch):

View File

@@ -34,17 +34,6 @@
"climate": {
"air_conditioner": {
"state_attributes": {
"preset_mode": {
"state": {
"wind_free": "mdi:weather-dust",
"wind_free_sleep": "mdi:sleep",
"quiet": "mdi:volume-off",
"long_wind": "mdi:weather-windy",
"smart": "mdi:leaf",
"motion_direct": "mdi:account-arrow-left",
"motion_indirect": "mdi:account-arrow-right"
}
},
"fan_mode": {
"state": {
"turbo": "mdi:wind-power"

View File

@@ -87,7 +87,7 @@
"wind_free_sleep": "WindFree sleep",
"quiet": "Quiet",
"long_wind": "Long wind",
"smart": "Smart saver",
"smart": "Smart",
"motion_direct": "Motion direct",
"motion_indirect": "Motion indirect"
}

View File

@@ -744,11 +744,8 @@ class ZhaConfigFlowHandler(BaseZhaFlow, ConfigFlow, domain=DOMAIN):
# Without confirmation, discovery can automatically progress into parts of the
# config flow logic that interacts with hardware.
# Ignore Zeroconf discoveries during onboarding, as they may be in use already.
if user_input is not None or (
not onboarding.async_is_onboarded(self.hass)
and not zha_config_entries
and self.source != SOURCE_ZEROCONF
not onboarding.async_is_onboarded(self.hass) and not zha_config_entries
):
# Probe the radio type if we don't have one yet
if self._radio_mgr.radio_type is None:

View File

@@ -130,7 +130,7 @@ multidict>=6.0.2
backoff>=2.0
# ensure pydantic version does not float since it might have breaking changes
pydantic==2.12.0
pydantic==2.11.9
# Required for Python 3.12.4 compatibility (#119223).
mashumaro>=3.13.1

6
requirements_all.txt generated
View File

@@ -1628,7 +1628,7 @@ open-meteo==0.3.2
# homeassistant.components.open_router
# homeassistant.components.openai_conversation
openai==2.2.0
openai==1.99.5
# homeassistant.components.openerz
openerz-api==0.3.0
@@ -2135,7 +2135,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lamarzocco
pylamarzocco==2.1.2
pylamarzocco==2.1.1
# homeassistant.components.lastfm
pylast==5.1.0
@@ -2210,7 +2210,7 @@ pynetio==0.1.9.1
pynina==0.3.6
# homeassistant.components.nintendo_parental
pynintendoparental==1.1.1
pynintendoparental==1.0.1
# homeassistant.components.nobo_hub
pynobo==1.8.1

View File

@@ -15,7 +15,7 @@ license-expression==30.4.3
mock-open==1.4.0
mypy-dev==1.19.0a2
pre-commit==4.2.0
pydantic==2.12.0
pydantic==2.11.9
pylint==3.3.8
pylint-per-file-ignores==1.4.0
pipdeptree==2.26.1

View File

@@ -1399,7 +1399,7 @@ open-meteo==0.3.2
# homeassistant.components.open_router
# homeassistant.components.openai_conversation
openai==2.2.0
openai==1.99.5
# homeassistant.components.openerz
openerz-api==0.3.0
@@ -1783,7 +1783,7 @@ pykrakenapi==0.1.8
pykulersky==0.5.8
# homeassistant.components.lamarzocco
pylamarzocco==2.1.2
pylamarzocco==2.1.1
# homeassistant.components.lastfm
pylast==5.1.0
@@ -1846,7 +1846,7 @@ pynetgear==0.10.10
pynina==0.3.6
# homeassistant.components.nintendo_parental
pynintendoparental==1.1.1
pynintendoparental==1.0.1
# homeassistant.components.nobo_hub
pynobo==1.8.1

View File

@@ -155,7 +155,7 @@ multidict>=6.0.2
backoff>=2.0
# ensure pydantic version does not float since it might have breaking changes
pydantic==2.12.0
pydantic==2.11.9
# Required for Python 3.12.4 compatibility (#119223).
mashumaro>=3.13.1

View File

@@ -9,17 +9,12 @@ from airthings_ble import (
AirthingsDevice,
AirthingsDeviceType,
)
from bleak.backends.device import BLEDevice
from homeassistant.components.airthings_ble.const import DOMAIN
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceEntry,
DeviceRegistry,
)
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry
from tests.common import MockConfigEntry, MockEntity
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
@@ -33,15 +28,7 @@ def patch_async_setup_entry(return_value=True):
)
def patch_async_discovered_service_info(return_value: list[BluetoothServiceInfoBleak]):
"""Patch async_discovered_service_info to return given list."""
return patch(
"homeassistant.components.bluetooth.async_discovered_service_info",
return_value=return_value,
)
def patch_async_ble_device_from_address(return_value: BLEDevice | None):
def patch_async_ble_device_from_address(return_value: BluetoothServiceInfoBleak | None):
"""Patch async ble device from address to return a given value."""
return patch(
"homeassistant.components.bluetooth.async_ble_device_from_address",
@@ -114,27 +101,6 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak(
tx_power=0,
)
WAVE_ENHANCE_SERVICE_INFO = BluetoothServiceInfoBleak(
name="cc-cc-cc-cc-cc-cc",
address="cc:cc:cc:cc:cc:cc",
device=generate_ble_device(
address="cc:cc:cc:cc:cc:cc",
name="Airthings Wave Enhance",
),
rssi=-61,
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_data={},
service_uuids=[],
source="local",
advertisement=generate_advertisement_data(
manufacturer_data={820: b"\xe4/\xa5\xae\t\x00"},
service_uuids=[],
),
connectable=True,
time=0,
tx_power=0,
)
VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak(
name="cc-cc-cc-cc-cc-cc",
address="cc:cc:cc:cc:cc:cc",
@@ -245,26 +211,6 @@ WAVE_DEVICE_INFO = AirthingsDevice(
address="cc:cc:cc:cc:cc:cc",
)
WAVE_ENHANCE_DEVICE_INFO = AirthingsDevice(
manufacturer="Airthings AS",
hw_version="REV X",
sw_version="T-SUB-2.6.2-master+0",
model=AirthingsDeviceType.WAVE_ENHANCE_EU,
name="Airthings Wave Enhance",
identifier="123456",
sensors={
"lux": 25,
"battery": 85,
"humidity": 60.0,
"temperature": 21.0,
"co2": 500.0,
"voc": 155.0,
"pressure": 1020,
"noise": 40,
},
address="cc:cc:cc:cc:cc:cc",
)
TEMPERATURE_V1 = MockEntity(
unique_id="Airthings Wave Plus 123456_temperature",
name="Airthings Wave Plus 123456 Temperature",
@@ -301,32 +247,23 @@ VOC_V3 = MockEntity(
)
def create_entry(
hass: HomeAssistant,
service_info: BluetoothServiceInfoBleak,
device_info: AirthingsDevice,
) -> MockConfigEntry:
def create_entry(hass: HomeAssistant) -> MockConfigEntry:
"""Create a config entry."""
entry = MockConfigEntry(
domain=DOMAIN,
unique_id=service_info.address,
title=f"{device_info.name} ({device_info.identifier})",
unique_id=WAVE_SERVICE_INFO.address,
title="Airthings Wave Plus (123456)",
)
entry.add_to_hass(hass)
return entry
def create_device(
entry: ConfigEntry,
device_registry: DeviceRegistry,
service_info: BluetoothServiceInfoBleak,
device_info: AirthingsDevice,
) -> DeviceEntry:
def create_device(entry: ConfigEntry, device_registry: DeviceRegistry):
"""Create a device for the given entry."""
return device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_BLUETOOTH, service_info.address)},
connections={(CONNECTION_BLUETOOTH, WAVE_SERVICE_INFO.address)},
manufacturer="Airthings AS",
name=f"{device_info.name} ({device_info.identifier})",
model=device_info.model.product_name,
name="Airthings Wave Plus (123456)",
model="Wave Plus",
)

View File

@@ -2,8 +2,6 @@
import logging
import pytest
from homeassistant.components.airthings_ble.const import DOMAIN
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -18,15 +16,10 @@ from . import (
VOC_V2,
VOC_V3,
WAVE_DEVICE_INFO,
WAVE_ENHANCE_DEVICE_INFO,
WAVE_ENHANCE_SERVICE_INFO,
WAVE_SERVICE_INFO,
create_device,
create_entry,
patch_airthings_ble,
patch_airthings_device_update,
patch_async_ble_device_from_address,
patch_async_discovered_service_info,
)
from tests.components.bluetooth import inject_bluetooth_service_info
@@ -40,8 +33,8 @@ async def test_migration_from_v1_to_v3_unique_id(
device_registry: dr.DeviceRegistry,
) -> None:
"""Verify that we can migrate from v1 (pre 2023.9.0) to the latest unique id format."""
entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
entry = create_entry(hass)
device = create_device(entry, device_registry)
assert entry is not None
assert device is not None
@@ -81,8 +74,8 @@ async def test_migration_from_v2_to_v3_unique_id(
device_registry: dr.DeviceRegistry,
) -> None:
"""Verify that we can migrate from v2 (introduced in 2023.9.0) to the latest unique id format."""
entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
entry = create_entry(hass)
device = create_device(entry, device_registry)
assert entry is not None
assert device is not None
@@ -122,8 +115,8 @@ async def test_migration_from_v1_and_v2_to_v3_unique_id(
device_registry: dr.DeviceRegistry,
) -> None:
"""Test if migration works when we have both v1 (pre 2023.9.0) and v2 (introduced in 2023.9.0) unique ids."""
entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
entry = create_entry(hass)
device = create_device(entry, device_registry)
assert entry is not None
assert device is not None
@@ -172,8 +165,8 @@ async def test_migration_with_all_unique_ids(
device_registry: dr.DeviceRegistry,
) -> None:
"""Test if migration works when we have all unique ids."""
entry = create_entry(hass, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
device = create_device(entry, device_registry, WAVE_SERVICE_INFO, WAVE_DEVICE_INFO)
entry = create_entry(hass)
device = create_device(entry, device_registry)
assert entry is not None
assert device is not None
@@ -222,48 +215,3 @@ async def test_migration_with_all_unique_ids(
assert entity_registry.async_get(v1.entity_id).unique_id == VOC_V1.unique_id
assert entity_registry.async_get(v2.entity_id).unique_id == VOC_V2.unique_id
assert entity_registry.async_get(v3.entity_id).unique_id == VOC_V3.unique_id
@pytest.mark.parametrize(
("unique_suffix", "expected_sensor_name"),
[
("lux", "Illuminance"),
("noise", "Ambient noise"),
],
)
async def test_translation_keys(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
device_registry: dr.DeviceRegistry,
unique_suffix: str,
expected_sensor_name: str,
) -> None:
"""Test that translated sensor names are correct."""
entry = create_entry(hass, WAVE_ENHANCE_SERVICE_INFO, WAVE_DEVICE_INFO)
device = create_device(
entry, device_registry, WAVE_ENHANCE_SERVICE_INFO, WAVE_ENHANCE_DEVICE_INFO
)
with (
patch_async_ble_device_from_address(WAVE_ENHANCE_SERVICE_INFO.device),
patch_async_discovered_service_info([WAVE_ENHANCE_SERVICE_INFO]),
patch_airthings_ble(WAVE_ENHANCE_DEVICE_INFO),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert device is not None
assert device.name == "Airthings Wave Enhance (123456)"
unique_id = f"{WAVE_ENHANCE_DEVICE_INFO.address}_{unique_suffix}"
entity_id = entity_registry.async_get_entity_id(Platform.SENSOR, DOMAIN, unique_id)
assert entity_id is not None
state = hass.states.get(entity_id)
assert state is not None
expected_value = WAVE_ENHANCE_DEVICE_INFO.sensors[unique_suffix]
assert state.state == str(expected_value)
expected_name = f"Airthings Wave Enhance (123456) {expected_sensor_name}"
assert state.attributes.get("friendly_name") == expected_name

View File

@@ -14,7 +14,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor.models import (
Discovery,
JobsInfo,
Repository,
ResolutionInfo,
StoreAddon,
@@ -510,13 +509,6 @@ def resolution_suggestions_for_issue_fixture(supervisor_client: AsyncMock) -> As
return supervisor_client.resolution.suggestions_for_issue
@pytest.fixture(name="jobs_info")
def jobs_info_fixture(supervisor_client: AsyncMock) -> AsyncMock:
"""Mock jobs info from supervisor."""
supervisor_client.jobs.info.return_value = JobsInfo(ignore_conditions=[], jobs=[])
return supervisor_client.jobs.info
@pytest.fixture(name="supervisor_client")
def supervisor_client() -> Generator[AsyncMock]:
"""Mock the supervisor client."""
@@ -562,10 +554,6 @@ def supervisor_client() -> Generator[AsyncMock]:
"homeassistant.components.hassio.issues.get_supervisor_client",
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.jobs.get_supervisor_client",
return_value=supervisor_client,
),
patch(
"homeassistant.components.hassio.repairs.get_supervisor_client",
return_value=supervisor_client,

View File

@@ -79,7 +79,6 @@ def all_setup_requests(
store_info: AsyncMock,
addon_changelog: AsyncMock,
addon_stats: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
include_addons = hasattr(request, "param") and request.param.get(
@@ -262,8 +261,3 @@ def all_setup_requests(
},
},
)
aioclient_mock.get(
"http://127.0.0.1/jobs/info",
json={"result": "ok", "data": {"ignore_conditions": [], "jobs": []}},
)

View File

@@ -26,7 +26,6 @@ def mock_all(
addon_changelog: AsyncMock,
addon_stats: AsyncMock,
resolution_info: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})

View File

@@ -25,7 +25,6 @@ def mock_all(
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})

View File

@@ -72,7 +72,6 @@ def mock_all(
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@@ -233,7 +232,7 @@ async def test_setup_api_ping(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert get_core_info(hass)["version_latest"] == "1.0.0"
assert is_hassio(hass)
@@ -280,7 +279,7 @@ async def test_setup_api_push_api_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert not aioclient_mock.mock_calls[0][2]["ssl"]
assert aioclient_mock.mock_calls[0][2]["port"] == 9999
assert "watchdog" not in aioclient_mock.mock_calls[0][2]
@@ -301,7 +300,7 @@ async def test_setup_api_push_api_data_server_host(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert not aioclient_mock.mock_calls[0][2]["ssl"]
assert aioclient_mock.mock_calls[0][2]["port"] == 9999
assert not aioclient_mock.mock_calls[0][2]["watchdog"]
@@ -322,7 +321,7 @@ async def test_setup_api_push_api_data_default(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert not aioclient_mock.mock_calls[0][2]["ssl"]
assert aioclient_mock.mock_calls[0][2]["port"] == 8123
refresh_token = aioclient_mock.mock_calls[0][2]["refresh_token"]
@@ -403,7 +402,7 @@ async def test_setup_api_existing_hassio_user(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert not aioclient_mock.mock_calls[0][2]["ssl"]
assert aioclient_mock.mock_calls[0][2]["port"] == 8123
assert aioclient_mock.mock_calls[0][2]["refresh_token"] == token.token
@@ -422,7 +421,7 @@ async def test_setup_core_push_config(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert aioclient_mock.mock_calls[1][2]["timezone"] == "testzone"
with patch("homeassistant.util.dt.set_default_time_zone"):
@@ -446,7 +445,7 @@ async def test_setup_hassio_no_additional_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
@@ -528,14 +527,14 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 23
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 22
assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 25
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 24
await hass.services.async_call("hassio", "backup_full", {})
await hass.services.async_call(
@@ -550,7 +549,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 27
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 26
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 03:48:00",
"homeassistant": True,
@@ -575,7 +574,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 29
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@@ -594,7 +593,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 29
assert aioclient_mock.mock_calls[-1][2] == {
"name": "backup_name",
"location": "backup_share",
@@ -610,7 +609,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 31
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 03:48:00",
"location": None,
@@ -629,7 +628,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 33
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 32
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 11:48:00",
"location": None,
@@ -1075,7 +1074,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done(wait_background_tasks=True)
assert result
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 19
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 18
assert len(mock_setup_entry.mock_calls) == 1

View File

@@ -34,7 +34,6 @@ def mock_all(
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
_install_default_mocks(aioclient_mock)

View File

@@ -60,7 +60,6 @@ def mock_all(
addon_changelog: AsyncMock,
addon_stats: AsyncMock,
resolution_info: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})

View File

@@ -1,10 +1,9 @@
"""The tests for the hassio update entities."""
from datetime import datetime, timedelta
from datetime import timedelta
import os
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4
from aiohasupervisor import (
SupervisorBadRequestError,
@@ -13,8 +12,6 @@ from aiohasupervisor import (
)
from aiohasupervisor.models import (
HomeAssistantUpdateOptions,
Job,
JobsInfo,
OSUpdate,
StoreAddonUpdate,
)
@@ -47,7 +44,6 @@ def mock_all(
addon_stats: AsyncMock,
addon_changelog: AsyncMock,
resolution_info: AsyncMock,
jobs_info: AsyncMock,
) -> None:
"""Mock all setup requests."""
aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"})
@@ -247,131 +243,6 @@ async def test_update_addon(hass: HomeAssistant, update_addon: AsyncMock) -> Non
update_addon.assert_called_once_with("test", StoreAddonUpdate(backup=False))
async def test_update_addon_progress(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test progress reporting for addon update."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
client = await hass_ws_client(hass)
message_id = 0
job_uuid = uuid4().hex
def make_job_message(progress: float, done: bool | None):
nonlocal message_id
message_id += 1
return {
"id": message_id,
"type": "supervisor/event",
"data": {
"event": "job",
"data": {
"uuid": job_uuid,
"created": "2025-09-29T00:00:00.000000+00:00",
"name": "addon_manager_update",
"reference": "test",
"progress": progress,
"done": done,
"stage": None,
"extra": {"total": 1234567890} if progress > 0 else None,
"errors": [],
},
},
}
await client.send_json(make_job_message(progress=0, done=None))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert hass.states.get("update.test_update").attributes.get("in_progress") is False
assert (
hass.states.get("update.test_update").attributes.get("update_percentage")
is None
)
await client.send_json(make_job_message(progress=5, done=False))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert hass.states.get("update.test_update").attributes.get("in_progress") is True
assert (
hass.states.get("update.test_update").attributes.get("update_percentage") == 5
)
await client.send_json(make_job_message(progress=50, done=False))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert hass.states.get("update.test_update").attributes.get("in_progress") is True
assert (
hass.states.get("update.test_update").attributes.get("update_percentage") == 50
)
await client.send_json(make_job_message(progress=100, done=True))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert hass.states.get("update.test_update").attributes.get("in_progress") is False
assert (
hass.states.get("update.test_update").attributes.get("update_percentage")
is None
)
async def test_addon_update_progress_startup(
hass: HomeAssistant, jobs_info: AsyncMock
) -> None:
"""Test addon update in progress during home assistant startup."""
jobs_info.return_value = JobsInfo(
ignore_conditions=[],
jobs=[
Job(
name="addon_manager_update",
reference="test",
uuid=uuid4().hex,
progress=50,
stage=None,
done=False,
errors=[],
created=datetime.now(),
child_jobs=[],
extra={"total": 1234567890},
)
],
)
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
assert hass.states.get("update.test_update").attributes.get("in_progress") is True
assert (
hass.states.get("update.test_update").attributes.get("update_percentage") == 50
)
async def setup_backup_integration(hass: HomeAssistant) -> None:
"""Set up the backup integration."""
assert await async_setup_component(hass, "backup", {})
@@ -759,186 +630,6 @@ async def test_update_core(hass: HomeAssistant, supervisor_client: AsyncMock) ->
)
async def test_update_core_progress(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test progress reporting for core update."""
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
client = await hass_ws_client(hass)
message_id = 0
job_uuid = uuid4().hex
def make_job_message(
progress: float, done: bool | None, errors: list[dict[str, str]] | None = None
):
nonlocal message_id
message_id += 1
return {
"id": message_id,
"type": "supervisor/event",
"data": {
"event": "job",
"data": {
"uuid": job_uuid,
"created": "2025-09-29T00:00:00.000000+00:00",
"name": "home_assistant_core_update",
"reference": None,
"progress": progress,
"done": done,
"stage": None,
"extra": {"total": 1234567890} if progress > 0 else None,
"errors": errors if errors else [],
},
},
}
await client.send_json(make_job_message(progress=0, done=None))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"in_progress"
)
is False
)
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"update_percentage"
)
is None
)
await client.send_json(make_job_message(progress=5, done=False))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"in_progress"
)
is True
)
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"update_percentage"
)
== 5
)
await client.send_json(make_job_message(progress=50, done=False))
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"in_progress"
)
is True
)
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"update_percentage"
)
== 50
)
# During a successful update Home Assistant is stopped before the update job
# reaches the end. An error ends it early so we use that for test
await client.send_json(
make_job_message(
progress=70,
done=True,
errors=[
{"type": "HomeAssistantUpdateError", "message": "bad", "stage": None}
],
)
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"in_progress"
)
is False
)
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"update_percentage"
)
is None
)
async def test_core_update_progress_startup(
hass: HomeAssistant, jobs_info: AsyncMock
) -> None:
"""Test core update in progress during home assistant startup.
This is an odd test, it's very unlikely core will be starting during an update.
It is technically possible though as core isn't stopped until the docker portion
is complete and updates can be started from CLI.
"""
jobs_info.return_value = JobsInfo(
ignore_conditions=[],
jobs=[
Job(
name="home_assistant_core_update",
reference=None,
uuid=uuid4().hex,
progress=50,
stage=None,
done=False,
errors=[],
created=datetime.now(),
child_jobs=[],
extra={"total": 1234567890},
)
],
)
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
with patch.dict(os.environ, MOCK_ENVIRON):
result = await async_setup_component(
hass,
"hassio",
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
)
assert result
await hass.async_block_till_done()
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"in_progress"
)
is True
)
assert (
hass.states.get("update.home_assistant_core_update").attributes.get(
"update_percentage"
)
== 50
)
@pytest.mark.parametrize(
("commands", "default_mount", "expected_kwargs"),
[

View File

@@ -4,6 +4,7 @@ from collections.abc import Generator
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
from pynintendoparental.device import Device
import pytest
from homeassistant.components.nintendo_parental.const import DOMAIN
@@ -23,6 +24,18 @@ def mock_config_entry() -> MockConfigEntry:
)
@pytest.fixture
def mock_nintendo_device() -> Device:
"""Return a mocked device."""
mock = AsyncMock(spec=Device)
mock.device_id = "testdevid"
mock.name = "Home Assistant Test"
mock.extra = {"device": {"firmwareVersion": {"displayedVersion": "99.99.99"}}}
mock.limit_time = 120
mock.today_playing_time = 110
return mock
@pytest.fixture
def mock_nintendo_authenticator() -> Generator[MagicMock]:
"""Mock Nintendo Authenticator."""
@@ -53,6 +66,27 @@ def mock_nintendo_authenticator() -> Generator[MagicMock]:
yield mock_auth
@pytest.fixture
def mock_nintendo_client(
mock_nintendo_device: Device,
) -> Generator[AsyncMock]:
"""Mock a Nintendo client."""
with (
patch(
"homeassistant.components.nintendo_parental.NintendoParental",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.nintendo_parental.config_flow.NintendoParental",
new=mock_client,
),
):
client = mock_client.return_value
client.update.return_value = True
client.devices.return_value = {"testdevid": mock_nintendo_device}
yield client
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""

View File

@@ -77,7 +77,7 @@ async def test_invalid_auth(
# Simulate invalid authentication by raising an exception
mock_nintendo_authenticator.complete_login.side_effect = (
InvalidSessionTokenException(status_code=401, message="Test")
InvalidSessionTokenException
)
result = await hass.config_entries.flow.async_configure(

View File

@@ -157,7 +157,6 @@ def create_function_tool_call_item(
ResponseFunctionCallArgumentsDoneEvent(
arguments="".join(arguments),
item_id=id,
name=name,
output_index=output_index,
sequence_number=0,
type="response.function_call_arguments.done",

View File

@@ -474,7 +474,7 @@ async def test_web_search(
assert mock_create_stream.mock_calls[0][2]["tools"] == [
{
"type": "web_search",
"type": "web_search_preview",
"search_context_size": "low",
"user_location": {
"type": "approximate",

View File

@@ -404,7 +404,6 @@ async def test_rpc_device_services(
)
assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON
mock_rpc_device.switch_set.assert_called_once_with(0, True)
monkeypatch.setitem(mock_rpc_device.status["switch:0"], "output", False)
await hass.services.async_call(
@@ -416,7 +415,6 @@ async def test_rpc_device_services(
mock_rpc_device.mock_update()
assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF
mock_rpc_device.switch_set.assert_called_with(0, False)
async def test_rpc_device_unique_ids(
@@ -509,7 +507,7 @@ async def test_rpc_set_state_errors(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test RPC device set state connection/call errors."""
mock_rpc_device.switch_set.side_effect = exc
monkeypatch.setattr(mock_rpc_device, "call_rpc", AsyncMock(side_effect=exc))
monkeypatch.delitem(mock_rpc_device.status, "cover:0")
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
await init_integration(hass, 2)
@@ -527,7 +525,11 @@ async def test_rpc_auth_error(
hass: HomeAssistant, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test RPC device set state authentication error."""
mock_rpc_device.switch_set.side_effect = InvalidAuthError
monkeypatch.setattr(
mock_rpc_device,
"call_rpc",
AsyncMock(side_effect=InvalidAuthError),
)
monkeypatch.delitem(mock_rpc_device.status, "cover:0")
monkeypatch.setitem(mock_rpc_device.status["sys"], "relay_in_thermostat", False)
entry = await init_integration(hass, 2)
@@ -655,7 +657,6 @@ async def test_rpc_device_virtual_switch(
mock_rpc_device.mock_update()
assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF
mock_rpc_device.boolean_set.assert_called_once_with(200, False)
monkeypatch.setitem(mock_rpc_device.status["boolean:200"], "value", True)
await hass.services.async_call(
@@ -667,7 +668,6 @@ async def test_rpc_device_virtual_switch(
mock_rpc_device.mock_update()
assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON
mock_rpc_device.boolean_set.assert_called_with(200, True)
@pytest.mark.usefixtures("disable_async_remove_shelly_rpc_entities")
@@ -815,7 +815,6 @@ async def test_rpc_device_script_switch(
assert (state := hass.states.get(entity_id))
assert state.state == STATE_OFF
mock_rpc_device.script_stop.assert_called_once_with(1)
monkeypatch.setitem(mock_rpc_device.status[key], "running", True)
await hass.services.async_call(
@@ -828,4 +827,3 @@ async def test_rpc_device_script_switch(
assert (state := hass.states.get(entity_id))
assert state.state == STATE_ON
mock_rpc_device.script_start.assert_called_once_with(1)

View File

@@ -4,7 +4,7 @@
'attributes': ReadOnlyDict({
'entity_picture': 'https://i.ytimg.com/vi/wysukDrMdqU/maxresdefault.jpg',
'friendly_name': 'Google for Developers Latest upload',
'published_at': datetime.datetime(2023, 5, 11, 0, 20, 46, tzinfo=TzInfo(0)),
'published_at': datetime.datetime(2023, 5, 11, 0, 20, 46, tzinfo=TzInfo(UTC)),
'video_id': 'wysukDrMdqU',
}),
'context': <ANY>,

View File

@@ -952,33 +952,6 @@ async def test_zeroconf_discovery_via_socket_already_setup_with_ip_match(
assert result["reason"] == "single_instance_allowed"
@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True))
async def test_zeroconf_not_onboarded(hass: HomeAssistant) -> None:
"""Test zeroconf discovery needing confirmation when not onboarded."""
service_info = ZeroconfServiceInfo(
ip_address=ip_address("192.168.1.100"),
ip_addresses=[ip_address("192.168.1.100")],
hostname="tube-zigbee-gw.local.",
name="mock_name",
port=6638,
properties={"name": "tube_123456"},
type="mock_type",
)
with patch(
"homeassistant.components.onboarding.async_is_onboarded", return_value=False
):
result_create = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=service_info,
)
await hass.async_block_till_done()
# not automatically confirmed
assert result_create["type"] is FlowResultType.FORM
assert result_create["step_id"] == "confirm"
@patch(
"homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type",
mock_detect_radio_type(radio_type=RadioType.deconz),