mirror of
https://github.com/home-assistant/core.git
synced 2025-10-10 12:19:31 +00:00
Compare commits
1 Commits
progress-a
...
water_hier
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03b58a4c21 |
@@ -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
|
||||
|
@@ -41,9 +41,6 @@
|
||||
},
|
||||
"illuminance": {
|
||||
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
|
||||
},
|
||||
"ambient_noise": {
|
||||
"name": "Ambient noise"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 = {
|
||||
|
@@ -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³)
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
@@ -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"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.1.2"]
|
||||
"requirements": ["pylamarzocco==2.1.1"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoparental==1.1.1"]
|
||||
"requirements": ["pynintendoparental==1.0.1"]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
),
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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):
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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
6
requirements_all.txt
generated
@@ -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
|
||||
|
@@ -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
|
||||
|
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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": []}},
|
||||
)
|
||||
|
@@ -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"})
|
||||
|
@@ -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"})
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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"})
|
||||
|
@@ -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"),
|
||||
[
|
||||
|
@@ -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."""
|
||||
|
@@ -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(
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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)
|
||||
|
@@ -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>,
|
||||
|
@@ -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),
|
||||
|
Reference in New Issue
Block a user