forked from home-assistant/core
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f75e6e5257 | |||
| ff17d79e73 | |||
| a8201009f3 | |||
| a349653282 | |||
| 355ee1178e | |||
| 30c5df3eaa | |||
| 10874af19a | |||
| 704118b3d0 | |||
| 7c575d0316 | |||
| ab3f11bfe7 | |||
| f0357539ad | |||
| e70a2dd257 | |||
| 5ef99a15a5 | |||
| 6421973cd6 | |||
| 7201171eb5 | |||
| 1fb438fa6c | |||
| 89ae68c5af | |||
| c78b66d5d5 | |||
| d756cf91ce | |||
| 8d13bf93ab | |||
| e86e793842 | |||
| 7e6bb021ce | |||
| 680b70aa29 | |||
| 8eebebc586 | |||
| 48e4624ba0 | |||
| b0cf974b34 | |||
| 171f7c5f81 |
@@ -64,16 +64,19 @@ jobs:
|
||||
You are a language detection system. Your task is to determine if the provided text is written in English or another language.
|
||||
|
||||
Rules:
|
||||
1. Analyze the text and determine the primary language
|
||||
1. Analyze the text and determine the primary language of the USER'S DESCRIPTION only
|
||||
2. IGNORE markdown headers (lines starting with #, ##, ###, etc.) as these are from issue templates, not user input
|
||||
3. IGNORE all code blocks (text between ``` or ` markers) as they may contain system-generated error messages in other languages
|
||||
4. Consider technical terms, code snippets, and URLs as neutral (they don't indicate non-English)
|
||||
5. Focus on the actual sentences and descriptions written by the user
|
||||
6. Return ONLY a JSON object with two fields:
|
||||
- "is_english": boolean (true if the text is primarily in English, false otherwise)
|
||||
4. IGNORE error messages, logs, and system output even if not in code blocks - these often appear in the user's system language
|
||||
5. Consider technical terms, code snippets, URLs, and file paths as neutral (they don't indicate non-English)
|
||||
6. Focus ONLY on the actual sentences and descriptions written by the user explaining their issue
|
||||
7. If the user's explanation/description is in English but includes non-English error messages or logs, consider it ENGLISH
|
||||
8. Return ONLY a JSON object with two fields:
|
||||
- "is_english": boolean (true if the user's description is primarily in English, false otherwise)
|
||||
- "detected_language": string (the name of the detected language, e.g., "English", "Spanish", "Chinese", etc.)
|
||||
7. Be lenient - if the text is mostly English with minor non-English elements, consider it English
|
||||
8. Common programming terms, error messages, and technical jargon should not be considered as non-English
|
||||
9. Be lenient - if the user's explanation is in English with non-English system output, it's still English
|
||||
10. Common programming terms, error messages, and technical jargon should not be considered as non-English
|
||||
11. If you cannot reliably determine the language, set detected_language to "undefined"
|
||||
|
||||
Example response:
|
||||
{"is_english": false, "detected_language": "Spanish"}
|
||||
@@ -122,6 +125,12 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// If language is undefined or not detected, skip processing
|
||||
if (!languageResult.detected_language || languageResult.detected_language === 'undefined') {
|
||||
console.log('Language could not be determined, skipping processing');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Issue detected as non-English: ${languageResult.detected_language}`);
|
||||
|
||||
// Post comment explaining the language requirement
|
||||
|
||||
Generated
+2
-2
@@ -1274,8 +1274,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rehlko/ @bdraco @peterager
|
||||
/homeassistant/components/remote/ @home-assistant/core
|
||||
/tests/components/remote/ @home-assistant/core
|
||||
/homeassistant/components/remote_calendar/ @Thomas55555
|
||||
/tests/components/remote_calendar/ @Thomas55555
|
||||
/homeassistant/components/remote_calendar/ @Thomas55555 @allenporter
|
||||
/tests/components/remote_calendar/ @Thomas55555 @allenporter
|
||||
/homeassistant/components/renault/ @epenet
|
||||
/tests/components/renault/ @epenet
|
||||
/homeassistant/components/renson/ @jimmyd-be
|
||||
|
||||
+20
-25
@@ -613,39 +613,34 @@ async def async_enable_logging(
|
||||
),
|
||||
)
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
if log_file is None:
|
||||
if "SUPERVISOR" in os.environ:
|
||||
_LOGGER.info("Running in Supervisor, not logging to file")
|
||||
err_log_path = None
|
||||
else:
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||
else:
|
||||
err_log_path = os.path.abspath(log_file)
|
||||
|
||||
if err_log_path:
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
err_dir = os.path.dirname(err_log_path)
|
||||
err_path_exists = os.path.isfile(err_log_path)
|
||||
err_dir = os.path.dirname(err_log_path)
|
||||
|
||||
# Check if we can write to the error log if it exists or that
|
||||
# we can create files in the containing directory if not.
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||
):
|
||||
err_handler = await hass.async_add_executor_job(
|
||||
_create_log_file, err_log_path, log_rotate_days
|
||||
)
|
||||
# Check if we can write to the error log if it exists or that
|
||||
# we can create files in the containing directory if not.
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||
):
|
||||
err_handler = await hass.async_add_executor_job(
|
||||
_create_log_file, err_log_path, log_rotate_days
|
||||
)
|
||||
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||
logger.addHandler(err_handler)
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
hass.data[DATA_LOGGING] = err_log_path
|
||||
else:
|
||||
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(err_handler)
|
||||
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
hass.data[DATA_LOGGING] = err_log_path
|
||||
else:
|
||||
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
|
||||
|
||||
async_activate_log_queue_handler(hass)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from jaraco.abode.exceptions import Exception as AbodeException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
@@ -70,6 +70,7 @@ def _trigger_automation(call: ServiceCall) -> None:
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
"requirements": ["aioamazondevices==3.1.2"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
@@ -15,6 +15,7 @@ from .const import CAMERAS, DATA_AMCREST, DOMAIN
|
||||
from .helpers import service_signal
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Amcrest IP Camera services."""
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import threading
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
|
||||
@@ -141,6 +141,7 @@ def download_file(service: ServiceCall) -> None:
|
||||
threading.Thread(target=do_download).start()
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the services for the downloader component."""
|
||||
async_register_admin_service(
|
||||
|
||||
@@ -63,6 +63,7 @@ def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).set_time(dt_util.now())
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
@@ -35,6 +35,7 @@ async def _async_service_handle(service: ServiceCall) -> None:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register FFmpeg services."""
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250531.2"]
|
||||
"requirements": ["home-assistant-frontend==20250531.3"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -49,6 +50,7 @@ async def _send_text_command(call: ServiceCall) -> ServiceResponse:
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Assistant SDK."""
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -77,6 +78,7 @@ def _read_file_contents(
|
||||
return results
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Google Photos services."""
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
@@ -76,6 +76,7 @@ async def _async_append_to_sheet(call: ServiceCall) -> None:
|
||||
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -249,6 +250,7 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
||||
return entry
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
"""Set up services for Habitica integration."""
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
from aiohue import HueBridgeV1, HueBridgeV2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import verify_domain_control
|
||||
|
||||
@@ -25,6 +25,7 @@ from .const import (
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for Hue integration."""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
@@ -115,6 +115,7 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount:
|
||||
return icloud_account
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register iCloud services."""
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["hdate"],
|
||||
"requirements": ["hdate[astral]==1.1.1"],
|
||||
"requirements": ["hdate[astral]==1.1.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
|
||||
translation_key="weekly_portion",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options_fn=lambda _: [str(p) for p in Parasha],
|
||||
value_fn=lambda results: str(results.after_tzais_date.upcoming_shabbat.parasha),
|
||||
value_fn=lambda results: results.after_tzais_date.upcoming_shabbat.parasha,
|
||||
),
|
||||
JewishCalendarSensorDescription(
|
||||
key="holiday",
|
||||
@@ -98,17 +98,13 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
|
||||
key="omer_count",
|
||||
translation_key="omer_count",
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda results: (
|
||||
results.after_shkia_date.omer.total_days
|
||||
if results.after_shkia_date.omer
|
||||
else 0
|
||||
),
|
||||
value_fn=lambda results: results.after_shkia_date.omer.total_days,
|
||||
),
|
||||
JewishCalendarSensorDescription(
|
||||
key="daf_yomi",
|
||||
translation_key="daf_yomi",
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda results: str(results.daytime_date.daf_yomi),
|
||||
value_fn=lambda results: results.daytime_date.daf_yomi,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -39,6 +40,7 @@ OMER_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Jewish Calendar services."""
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"requirements": ["pypck==0.8.6", "lcn-frontend==0.2.5"]
|
||||
"requirements": ["pypck==0.8.7", "lcn-frontend==0.2.5"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
@@ -438,6 +439,7 @@ SERVICES = (
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for LCN."""
|
||||
for service_name, service in SERVICES:
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.11"],
|
||||
"requirements": ["python-linkplay==0.2.12"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -36,11 +36,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PRODID = "-//homeassistant.io//local_calendar 1.0//EN"
|
||||
|
||||
# The calendar on disk is only changed when this entity is updated, so there
|
||||
# is no need to poll for changes. The calendar enttiy base class will handle
|
||||
# refreshing the entity state based on the start or end time of the event.
|
||||
SCAN_INTERVAL = timedelta(days=1)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -66,6 +67,7 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> NordPoolConfigEntry:
|
||||
return entry
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for Nord Pool integration."""
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -48,6 +48,7 @@ def set_speed(call: ServiceCall) -> None:
|
||||
_get_coordinator(call).nzbget.rate(call.data[ATTR_SPEED])
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register integration-level services."""
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import selector
|
||||
@@ -70,6 +71,7 @@ def __get_client(call: ServiceCall) -> OhmeApiClient:
|
||||
return entry.runtime_data.charge_session_coordinator.client
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services."""
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -70,6 +71,7 @@ def _read_file_contents(
|
||||
return results
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register OneDrive services."""
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -40,6 +40,7 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema(
|
||||
SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Onkyo services."""
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TIME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -46,6 +46,98 @@ from .const import (
|
||||
if TYPE_CHECKING:
|
||||
from . import OpenThermGatewayHub
|
||||
|
||||
_SERVICE_RESET_SCHEMA = vol.Schema({vol.Required(ATTR_GW_ID): vol.All(cv.string)})
|
||||
_SERVICE_SET_CENTRAL_HEATING_OVRD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_CH_OVRD): cv.boolean,
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_CLOCK_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Optional(ATTR_DATE, default=date.today): cv.date,
|
||||
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_CONTROL_SETPOINT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=90)
|
||||
),
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_HOT_WATER_SETPOINT_SCHEMA = _SERVICE_SET_CONTROL_SETPOINT_SCHEMA
|
||||
_SERVICE_SET_HOT_WATER_OVRD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_DHW_OVRD): vol.Any(
|
||||
vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
|
||||
),
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_GPIO_MODE_SCHEMA = vol.Schema(
|
||||
vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.Equal("A"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=6)
|
||||
),
|
||||
}
|
||||
),
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.Equal("B"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=7)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
_SERVICE_SET_LED_MODE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.In("ABCDEF"),
|
||||
vol.Required(ATTR_MODE): vol.In("RXTBOFHWCEMP"),
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_MAX_MOD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_LEVEL): vol.All(vol.Coerce(int), vol.Range(min=-1, max=100)),
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_OAT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=-40, max=99)
|
||||
),
|
||||
}
|
||||
)
|
||||
_SERVICE_SET_SB_TEMP_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=30)
|
||||
),
|
||||
}
|
||||
)
|
||||
_SERVICE_SEND_TRANSP_CMD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TRANSP_CMD): vol.All(
|
||||
cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
|
||||
),
|
||||
vol.Required(ATTR_TRANSP_ARG): vol.All(cv.string, vol.Length(min=1, max=12)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_gateway(call: ServiceCall) -> OpenThermGatewayHub:
|
||||
gw_id: str = call.data[ATTR_GW_ID]
|
||||
@@ -61,236 +153,154 @@ def _get_gateway(call: ServiceCall) -> OpenThermGatewayHub:
|
||||
return gw_hub
|
||||
|
||||
|
||||
async def _reset_gateway(call: ServiceCall) -> None:
|
||||
"""Reset the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
mode_rst = gw_vars.OTGW_MODE_RESET
|
||||
await gw_hub.gateway.set_mode(mode_rst)
|
||||
|
||||
|
||||
async def _set_ch_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the central heating override on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0)
|
||||
|
||||
|
||||
async def _set_control_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the control setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
|
||||
async def _set_dhw_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water override on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
|
||||
|
||||
|
||||
async def _set_dhw_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
|
||||
async def _set_device_clock(call: ServiceCall) -> None:
|
||||
"""Set the clock on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
attr_date = call.data[ATTR_DATE]
|
||||
attr_time = call.data[ATTR_TIME]
|
||||
await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
|
||||
|
||||
|
||||
async def _set_gpio_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway GPIO modes."""
|
||||
gw_hub = _get_gateway(call)
|
||||
gpio_id = call.data[ATTR_ID]
|
||||
gpio_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
|
||||
|
||||
|
||||
async def _set_led_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway LED modes."""
|
||||
gw_hub = _get_gateway(call)
|
||||
led_id = call.data[ATTR_ID]
|
||||
led_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_led_mode(led_id, led_mode)
|
||||
|
||||
|
||||
async def _set_max_mod(call: ServiceCall) -> None:
|
||||
"""Set the max modulation level."""
|
||||
gw_hub = _get_gateway(call)
|
||||
level = call.data[ATTR_LEVEL]
|
||||
if level == -1:
|
||||
# Backend only clears setting on non-numeric values.
|
||||
level = "-"
|
||||
await gw_hub.gateway.set_max_relative_mod(level)
|
||||
|
||||
|
||||
async def _set_outside_temp(call: ServiceCall) -> None:
|
||||
"""Provide the outside temperature to the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
|
||||
async def _set_setback_temp(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway SetBack temperature."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
|
||||
async def _send_transparent_cmd(call: ServiceCall) -> None:
|
||||
"""Send a transparent OpenTherm Gateway command."""
|
||||
gw_hub = _get_gateway(call)
|
||||
transp_cmd = call.data[ATTR_TRANSP_CMD]
|
||||
transp_arg = call.data[ATTR_TRANSP_ARG]
|
||||
await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the component."""
|
||||
service_reset_schema = vol.Schema({vol.Required(ATTR_GW_ID): vol.All(cv.string)})
|
||||
service_set_central_heating_ovrd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_CH_OVRD): cv.boolean,
|
||||
}
|
||||
)
|
||||
service_set_clock_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Optional(ATTR_DATE, default=date.today): cv.date,
|
||||
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
|
||||
}
|
||||
)
|
||||
service_set_control_setpoint_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=90)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_hot_water_setpoint_schema = service_set_control_setpoint_schema
|
||||
service_set_hot_water_ovrd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_DHW_OVRD): vol.Any(
|
||||
vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_gpio_mode_schema = vol.Schema(
|
||||
vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.Equal("A"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=6)
|
||||
),
|
||||
}
|
||||
),
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.Equal("B"),
|
||||
vol.Required(ATTR_MODE): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=0, max=7)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
service_set_led_mode_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_ID): vol.In("ABCDEF"),
|
||||
vol.Required(ATTR_MODE): vol.In("RXTBOFHWCEMP"),
|
||||
}
|
||||
)
|
||||
service_set_max_mod_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_LEVEL): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=-1, max=100)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_oat_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=-40, max=99)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_set_sb_temp_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TEMPERATURE): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=30)
|
||||
),
|
||||
}
|
||||
)
|
||||
service_send_transp_cmd_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GW_ID): vol.All(cv.string),
|
||||
vol.Required(ATTR_TRANSP_CMD): vol.All(
|
||||
cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
|
||||
),
|
||||
vol.Required(ATTR_TRANSP_ARG): vol.All(
|
||||
cv.string, vol.Length(min=1, max=12)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def reset_gateway(call: ServiceCall) -> None:
|
||||
"""Reset the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
mode_rst = gw_vars.OTGW_MODE_RESET
|
||||
await gw_hub.gateway.set_mode(mode_rst)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway, service_reset_schema
|
||||
DOMAIN, SERVICE_RESET_GATEWAY, _reset_gateway, _SERVICE_RESET_SCHEMA
|
||||
)
|
||||
|
||||
async def set_ch_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the central heating override on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CH_OVRD,
|
||||
set_ch_ovrd,
|
||||
service_set_central_heating_ovrd_schema,
|
||||
_set_ch_ovrd,
|
||||
_SERVICE_SET_CENTRAL_HEATING_OVRD_SCHEMA,
|
||||
)
|
||||
|
||||
async def set_control_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the control setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONTROL_SETPOINT,
|
||||
set_control_setpoint,
|
||||
service_set_control_setpoint_schema,
|
||||
_set_control_setpoint,
|
||||
_SERVICE_SET_CONTROL_SETPOINT_SCHEMA,
|
||||
)
|
||||
|
||||
async def set_dhw_ovrd(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water override on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_OVRD,
|
||||
set_dhw_ovrd,
|
||||
service_set_hot_water_ovrd_schema,
|
||||
_set_dhw_ovrd,
|
||||
_SERVICE_SET_HOT_WATER_OVRD_SCHEMA,
|
||||
)
|
||||
|
||||
async def set_dhw_setpoint(call: ServiceCall) -> None:
|
||||
"""Set the domestic hot water setpoint on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HOT_WATER_SETPOINT,
|
||||
set_dhw_setpoint,
|
||||
service_set_hot_water_setpoint_schema,
|
||||
_set_dhw_setpoint,
|
||||
_SERVICE_SET_HOT_WATER_SETPOINT_SCHEMA,
|
||||
)
|
||||
|
||||
async def set_device_clock(call: ServiceCall) -> None:
|
||||
"""Set the clock on the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
attr_date = call.data[ATTR_DATE]
|
||||
attr_time = call.data[ATTR_TIME]
|
||||
await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_CLOCK, set_device_clock, service_set_clock_schema
|
||||
DOMAIN, SERVICE_SET_CLOCK, _set_device_clock, _SERVICE_SET_CLOCK_SCHEMA
|
||||
)
|
||||
|
||||
async def set_gpio_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway GPIO modes."""
|
||||
gw_hub = _get_gateway(call)
|
||||
gpio_id = call.data[ATTR_ID]
|
||||
gpio_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema
|
||||
DOMAIN, SERVICE_SET_GPIO_MODE, _set_gpio_mode, _SERVICE_SET_GPIO_MODE_SCHEMA
|
||||
)
|
||||
|
||||
async def set_led_mode(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway LED modes."""
|
||||
gw_hub = _get_gateway(call)
|
||||
led_id = call.data[ATTR_ID]
|
||||
led_mode = call.data[ATTR_MODE]
|
||||
await gw_hub.gateway.set_led_mode(led_id, led_mode)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema
|
||||
DOMAIN, SERVICE_SET_LED_MODE, _set_led_mode, _SERVICE_SET_LED_MODE_SCHEMA
|
||||
)
|
||||
|
||||
async def set_max_mod(call: ServiceCall) -> None:
|
||||
"""Set the max modulation level."""
|
||||
gw_hub = _get_gateway(call)
|
||||
level = call.data[ATTR_LEVEL]
|
||||
if level == -1:
|
||||
# Backend only clears setting on non-numeric values.
|
||||
level = "-"
|
||||
await gw_hub.gateway.set_max_relative_mod(level)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, service_set_max_mod_schema
|
||||
DOMAIN, SERVICE_SET_MAX_MOD, _set_max_mod, _SERVICE_SET_MAX_MOD_SCHEMA
|
||||
)
|
||||
|
||||
async def set_outside_temp(call: ServiceCall) -> None:
|
||||
"""Provide the outside temperature to the OpenTherm Gateway."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_OAT, set_outside_temp, service_set_oat_schema
|
||||
DOMAIN, SERVICE_SET_OAT, _set_outside_temp, _SERVICE_SET_OAT_SCHEMA
|
||||
)
|
||||
|
||||
async def set_setback_temp(call: ServiceCall) -> None:
|
||||
"""Set the OpenTherm Gateway SetBack temperature."""
|
||||
gw_hub = _get_gateway(call)
|
||||
await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, service_set_sb_temp_schema
|
||||
DOMAIN, SERVICE_SET_SB_TEMP, _set_setback_temp, _SERVICE_SET_SB_TEMP_SCHEMA
|
||||
)
|
||||
|
||||
async def send_transparent_cmd(call: ServiceCall) -> None:
|
||||
"""Send a transparent OpenTherm Gateway command."""
|
||||
gw_hub = _get_gateway(call)
|
||||
transp_cmd = call.data[ATTR_TRANSP_CMD]
|
||||
transp_arg = call.data[ATTR_TRANSP_ARG]
|
||||
await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_TRANSP_CMD,
|
||||
send_transparent_cmd,
|
||||
service_send_transp_cmd_schema,
|
||||
_send_transparent_cmd,
|
||||
_SERVICE_SEND_TRANSP_CMD_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from opower import (
|
||||
CannotConnect,
|
||||
InvalidAuth,
|
||||
Opower,
|
||||
create_cookie_jar,
|
||||
get_supported_utility_names,
|
||||
select_utility,
|
||||
)
|
||||
@@ -39,7 +40,7 @@ async def _validate_login(
|
||||
) -> dict[str, str]:
|
||||
"""Validate login data and return any errors."""
|
||||
api = Opower(
|
||||
async_create_clientsession(hass),
|
||||
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
|
||||
login_data[CONF_UTILITY],
|
||||
login_data[CONF_USERNAME],
|
||||
login_data[CONF_PASSWORD],
|
||||
|
||||
@@ -12,6 +12,7 @@ from opower import (
|
||||
MeterType,
|
||||
Opower,
|
||||
ReadResolution,
|
||||
create_cookie_jar,
|
||||
)
|
||||
from opower.exceptions import ApiException, CannotConnect, InvalidAuth
|
||||
|
||||
@@ -30,7 +31,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import aiohttp_client, issue_registry as ir
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -62,7 +64,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
update_interval=timedelta(hours=12),
|
||||
)
|
||||
self.api = Opower(
|
||||
aiohttp_client.async_get_clientsession(hass),
|
||||
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
|
||||
config_entry.data[CONF_UTILITY],
|
||||
config_entry.data[CONF_USERNAME],
|
||||
config_entry.data[CONF_PASSWORD],
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.12.3"]
|
||||
"requirements": ["opower==0.12.4"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import cast
|
||||
from python_picnic_api2 import PicnicAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
@@ -26,6 +26,7 @@ class PicnicServiceException(Exception):
|
||||
"""Exception for Picnic services."""
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for the Picnic integration, if not registered yet."""
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import COMMANDS, DOMAIN, PS4_DATA
|
||||
@@ -29,6 +29,7 @@ async def async_service_command(call: ServiceCall) -> None:
|
||||
await device.async_send_command(command)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Handle for services."""
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import datetime
|
||||
import logging
|
||||
|
||||
from ical.event import Event
|
||||
from ical.timeline import Timeline
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -48,12 +49,18 @@ class RemoteCalendarEntity(
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = entry.data[CONF_CALENDAR_NAME]
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self._event: CalendarEvent | None = None
|
||||
self._timeline: Timeline | None = None
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return self._event
|
||||
if self._timeline is None:
|
||||
return None
|
||||
now = dt_util.now()
|
||||
events = self._timeline.active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
@@ -79,15 +86,12 @@ class RemoteCalendarEntity(
|
||||
"""
|
||||
await super().async_update()
|
||||
|
||||
def next_timeline_event() -> CalendarEvent | None:
|
||||
def _get_timeline() -> Timeline | None:
|
||||
"""Return the next active event."""
|
||||
now = dt_util.now()
|
||||
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
return self.coordinator.data.timeline_tz(now.tzinfo)
|
||||
|
||||
self._event = await self.hass.async_add_executor_job(next_timeline_event)
|
||||
self._timeline = await self.hass.async_add_executor_job(_get_timeline)
|
||||
|
||||
|
||||
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "remote_calendar",
|
||||
"name": "Remote Calendar",
|
||||
"codeowners": ["@Thomas55555"],
|
||||
"codeowners": ["@Thomas55555", "@allenporter"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/remote_calendar",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -63,6 +63,7 @@ BINARY_PUSH_SENSORS = (
|
||||
cmd_id=33,
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
value=lambda api, ch: api.motion_detected(ch),
|
||||
supported=lambda api, ch: api.supported(ch, "motion_detection"),
|
||||
),
|
||||
ReolinkBinarySensorEntityDescription(
|
||||
key=FACE_DETECTION_TYPE,
|
||||
|
||||
@@ -37,23 +37,27 @@ CAMERA_ENTITIES = (
|
||||
key="sub",
|
||||
stream="sub",
|
||||
translation_key="sub",
|
||||
supported=lambda api, ch: api.supported(ch, "stream"),
|
||||
),
|
||||
ReolinkCameraEntityDescription(
|
||||
key="main",
|
||||
stream="main",
|
||||
translation_key="main",
|
||||
supported=lambda api, ch: api.supported(ch, "stream"),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ReolinkCameraEntityDescription(
|
||||
key="snapshots_sub",
|
||||
stream="snapshots_sub",
|
||||
translation_key="snapshots_sub",
|
||||
supported=lambda api, ch: api.supported(ch, "snapshot"),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ReolinkCameraEntityDescription(
|
||||
key="snapshots",
|
||||
stream="snapshots_main",
|
||||
translation_key="snapshots_main",
|
||||
supported=lambda api, ch: api.supported(ch, "snapshot"),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
ReolinkCameraEntityDescription(
|
||||
|
||||
@@ -39,6 +39,8 @@ async def async_get_config_entry_diagnostics(
|
||||
"firmware version": api.sw_version,
|
||||
"HTTPS": api.use_https,
|
||||
"HTTP(S) port": api.port,
|
||||
"Baichuan port": api.baichuan.port,
|
||||
"Baichuan only": api.baichuan_only,
|
||||
"WiFi connection": api.wifi_connection,
|
||||
"WiFi signal": api.wifi_signal,
|
||||
"RTMP enabled": api.rtmp_enabled,
|
||||
|
||||
@@ -190,6 +190,7 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
via_device=(DOMAIN, self._host.unique_id),
|
||||
name=self._host.api.camera_name(dev_ch),
|
||||
model=self._host.api.camera_model(dev_ch),
|
||||
model_id=self._host.api.item_number(dev_ch),
|
||||
manufacturer=self._host.api.manufacturer,
|
||||
hw_version=self._host.api.camera_hardware_version(dev_ch),
|
||||
sw_version=self._host.api.camera_sw_version(dev_ch),
|
||||
|
||||
@@ -172,6 +172,9 @@
|
||||
"floodlight_brightness": {
|
||||
"default": "mdi:spotlight-beam"
|
||||
},
|
||||
"ir_brightness": {
|
||||
"default": "mdi:led-off"
|
||||
},
|
||||
"volume": {
|
||||
"default": "mdi:volume-high",
|
||||
"state": {
|
||||
|
||||
@@ -122,6 +122,20 @@ NUMBER_ENTITIES = (
|
||||
value=lambda api, ch: api.whiteled_brightness(ch),
|
||||
method=lambda api, ch, value: api.set_whiteled(ch, brightness=int(value)),
|
||||
),
|
||||
ReolinkNumberEntityDescription(
|
||||
key="ir_brightness",
|
||||
cmd_key="208",
|
||||
translation_key="ir_brightness",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_step=1,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
supported=lambda api, ch: api.supported(ch, "ir_brightness"),
|
||||
value=lambda api, ch: api.baichuan.ir_brightness(ch),
|
||||
method=lambda api, ch, value: (
|
||||
api.baichuan.set_status_led(ch, ir_brightness=int(value))
|
||||
),
|
||||
),
|
||||
ReolinkNumberEntityDescription(
|
||||
key="volume",
|
||||
cmd_key="GetAudioCfg",
|
||||
|
||||
@@ -532,6 +532,9 @@
|
||||
"floodlight_brightness": {
|
||||
"name": "Floodlight turn on brightness"
|
||||
},
|
||||
"ir_brightness": {
|
||||
"name": "Infrared light brightness"
|
||||
},
|
||||
"volume": {
|
||||
"name": "Volume"
|
||||
},
|
||||
|
||||
@@ -76,10 +76,10 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity)
|
||||
|
||||
def _wake_on_lan(self) -> None:
|
||||
"""Wake the device via wake on lan."""
|
||||
send_magic_packet(self._mac, ip_address=self._host)
|
||||
send_magic_packet(self._mac, ip_address=self._host) # type: ignore[arg-type]
|
||||
# If the ip address changed since we last saw the device
|
||||
# broadcast a packet as well
|
||||
send_magic_packet(self._mac)
|
||||
send_magic_packet(self._mac) # type: ignore[arg-type]
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"getmac==0.9.5",
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.7.2",
|
||||
"wakeonlan==2.1.0",
|
||||
"wakeonlan==3.1.0",
|
||||
"async-upnp-client==0.44.0"
|
||||
],
|
||||
"ssdp": [
|
||||
|
||||
@@ -35,7 +35,7 @@ from .coordinator import (
|
||||
SwissPublicTransportDataUpdateCoordinator,
|
||||
)
|
||||
from .helper import offset_opendata, unique_id_from_config
|
||||
from .services import setup_services
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,7 +47,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Swiss public transport component."""
|
||||
setup_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.core import (
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -39,7 +40,7 @@ SERVICE_FETCH_CONNECTIONS_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def async_get_entry(
|
||||
def _async_get_entry(
|
||||
hass: HomeAssistant, config_entry_id: str
|
||||
) -> SwissPublicTransportConfigEntry:
|
||||
"""Get the Swiss public transport config entry."""
|
||||
@@ -58,34 +59,36 @@ def async_get_entry(
|
||||
return entry
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
async def _async_fetch_connections(
|
||||
call: ServiceCall,
|
||||
) -> ServiceResponse:
|
||||
"""Fetch a set of connections."""
|
||||
config_entry = _async_get_entry(call.hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
|
||||
limit = call.data.get(ATTR_LIMIT) or CONNECTIONS_COUNT
|
||||
try:
|
||||
connections = await config_entry.runtime_data.fetch_connections_as_json(
|
||||
limit=int(limit)
|
||||
)
|
||||
except UpdateFailed as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"error": str(e),
|
||||
},
|
||||
) from e
|
||||
return {"connections": connections}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Swiss public transport integration."""
|
||||
|
||||
async def async_fetch_connections(
|
||||
call: ServiceCall,
|
||||
) -> ServiceResponse:
|
||||
"""Fetch a set of connections."""
|
||||
config_entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
|
||||
limit = call.data.get(ATTR_LIMIT) or CONNECTIONS_COUNT
|
||||
try:
|
||||
connections = await config_entry.runtime_data.fetch_connections_as_json(
|
||||
limit=int(limit)
|
||||
)
|
||||
except UpdateFailed as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={
|
||||
"error": str(e),
|
||||
},
|
||||
) from e
|
||||
return {"connections": connections}
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_FETCH_CONNECTIONS,
|
||||
async_fetch_connections,
|
||||
_async_fetch_connections,
|
||||
schema=SERVICE_FETCH_CONNECTIONS_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from voluptuous import All, Range
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
@@ -98,6 +98,7 @@ def async_get_energy_site_for_entry(
|
||||
return energy_data
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Teslemetry services."""
|
||||
|
||||
|
||||
@@ -303,6 +303,7 @@ SERVICES = [
|
||||
]
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the global UniFi Protect services."""
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@ from homeassistant.helpers import aiohttp_client
|
||||
async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Return a new aiohttp session."""
|
||||
return aiohttp_client.async_create_clientsession(
|
||||
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
|
||||
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
partial(wakeonlan.send_magic_packet, mac_address, **service_kwargs)
|
||||
partial(wakeonlan.send_magic_packet, mac_address, **service_kwargs) # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/wake_on_lan",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["wakeonlan==2.1.0"]
|
||||
"requirements": ["wakeonlan==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import voluptuous as vol
|
||||
from yolink.client_request import ClientRequest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
@@ -25,6 +25,7 @@ _SPEAKER_HUB_PLAY_CALL_OPTIONAL_ATTRS = (
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for YoLink integration."""
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ TARGET_VALIDATORS = {
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register integration services."""
|
||||
services = ZWaveServices(hass, er.async_get(hass), dr.async_get(hass))
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==3.49.0
|
||||
hass-nabucasa==0.101.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250531.2
|
||||
home-assistant-frontend==20250531.3
|
||||
home-assistant-intents==2025.6.10
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
Generated
+7
-7
@@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.0.6
|
||||
aioamazondevices==3.1.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -1133,7 +1133,7 @@ hass-splunk==0.1.1
|
||||
hassil==2.2.3
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate[astral]==1.1.1
|
||||
hdate[astral]==1.1.2
|
||||
|
||||
# homeassistant.components.heatmiser
|
||||
heatmiserV3==2.0.3
|
||||
@@ -1164,7 +1164,7 @@ hole==0.8.0
|
||||
holidays==0.74
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.2
|
||||
home-assistant-frontend==20250531.3
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.10
|
||||
@@ -1617,7 +1617,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.12.3
|
||||
opower==0.12.4
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -2236,7 +2236,7 @@ pypaperless==4.1.0
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.8.6
|
||||
pypck==0.8.7
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.5
|
||||
@@ -2452,7 +2452,7 @@ python-juicenet==1.1.0
|
||||
python-kasa[speedups]==0.10.2
|
||||
|
||||
# homeassistant.components.linkplay
|
||||
python-linkplay==0.2.11
|
||||
python-linkplay==0.2.12
|
||||
|
||||
# homeassistant.components.lirc
|
||||
# python-lirc==1.2.3
|
||||
@@ -3059,7 +3059,7 @@ vultr==0.1.2
|
||||
|
||||
# homeassistant.components.samsungtv
|
||||
# homeassistant.components.wake_on_lan
|
||||
wakeonlan==2.1.0
|
||||
wakeonlan==3.1.0
|
||||
|
||||
# homeassistant.components.wallbox
|
||||
wallbox==0.9.0
|
||||
|
||||
Generated
+7
-7
@@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.0.6
|
||||
aioamazondevices==3.1.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -988,7 +988,7 @@ hass-nabucasa==0.101.0
|
||||
hassil==2.2.3
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate[astral]==1.1.1
|
||||
hdate[astral]==1.1.2
|
||||
|
||||
# homeassistant.components.here_travel_time
|
||||
here-routing==1.0.1
|
||||
@@ -1010,7 +1010,7 @@ hole==0.8.0
|
||||
holidays==0.74
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.2
|
||||
home-assistant-frontend==20250531.3
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.10
|
||||
@@ -1370,7 +1370,7 @@ openhomedevice==2.2.0
|
||||
openwebifpy==4.3.1
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.12.3
|
||||
opower==0.12.4
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1857,7 +1857,7 @@ pypalazzetti==0.1.19
|
||||
pypaperless==4.1.0
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.8.6
|
||||
pypck==0.8.7
|
||||
|
||||
# homeassistant.components.pglab
|
||||
pypglab==0.0.5
|
||||
@@ -2022,7 +2022,7 @@ python-juicenet==1.1.0
|
||||
python-kasa[speedups]==0.10.2
|
||||
|
||||
# homeassistant.components.linkplay
|
||||
python-linkplay==0.2.11
|
||||
python-linkplay==0.2.12
|
||||
|
||||
# homeassistant.components.lirc
|
||||
# python-lirc==1.2.3
|
||||
@@ -2518,7 +2518,7 @@ vultr==0.1.2
|
||||
|
||||
# homeassistant.components.samsungtv
|
||||
# homeassistant.components.wake_on_lan
|
||||
wakeonlan==2.1.0
|
||||
wakeonlan==3.1.0
|
||||
|
||||
# homeassistant.components.wallbox
|
||||
wallbox==0.9.0
|
||||
|
||||
@@ -56,6 +56,9 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
|
||||
do_not_disturb=False,
|
||||
response_style=None,
|
||||
bluetooth_state=True,
|
||||
entity_id="11111111-2222-3333-4444-555555555555",
|
||||
appliance_id="G1234567890123456789012345678A",
|
||||
sensors={},
|
||||
)
|
||||
}
|
||||
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(
|
||||
|
||||
@@ -175,7 +175,12 @@ async def test_error_invalid_ssh(hass: HomeAssistant, patch_is_file) -> None:
|
||||
config_data = {k: v for k, v in CONFIG_DATA_SSH.items() if k != CONF_PASSWORD}
|
||||
config_data[CONF_SSH_KEY] = SSH_KEY
|
||||
|
||||
patch_is_file.return_value = False
|
||||
def mock_is_file(file) -> bool:
|
||||
if str(file).endswith(SSH_KEY):
|
||||
return False
|
||||
return True
|
||||
|
||||
patch_is_file.side_effect = mock_is_file
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER, "show_advanced_options": True},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test MatrixBot._login."""
|
||||
|
||||
from pydantic.dataclasses import dataclass
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.matrix import MatrixBot
|
||||
@@ -17,7 +18,7 @@ class LoginTestParameters:
|
||||
access_token: dict[str, str]
|
||||
expected_login_state: bool
|
||||
expected_caplog_messages: set[str]
|
||||
expected_expection: type(Exception) | None = None
|
||||
expected_expection: type[Exception] | None = None
|
||||
|
||||
|
||||
good_password_missing_token = LoginTestParameters(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""The tests for the panel_custom component."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components import frontend, panel_custom
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -22,14 +20,13 @@ async def test_webcomponent_custom_path_not_found(hass: HomeAssistant) -> None:
|
||||
}
|
||||
}
|
||||
|
||||
with patch("os.path.isfile", Mock(return_value=False)):
|
||||
result = await setup.async_setup_component(hass, "panel_custom", config)
|
||||
assert not result
|
||||
result = await setup.async_setup_component(hass, "panel_custom", config)
|
||||
assert not result
|
||||
|
||||
panels = hass.data.get(frontend.DATA_PANELS, [])
|
||||
panels = hass.data.get(frontend.DATA_PANELS, [])
|
||||
|
||||
assert panels
|
||||
assert "nice_url" not in panels
|
||||
assert panels
|
||||
assert "nice_url" not in panels
|
||||
|
||||
|
||||
async def test_js_webcomponent(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -143,6 +143,7 @@ def reolink_connect_class() -> Generator[MagicMock]:
|
||||
|
||||
# Baichuan
|
||||
host_mock.baichuan = create_autospec(Baichuan)
|
||||
host_mock.baichuan_only = False
|
||||
# Disable tcp push by default for tests
|
||||
host_mock.baichuan.port = TEST_BC_PORT
|
||||
host_mock.baichuan.events_active = False
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
'pushAlarm': 7,
|
||||
}),
|
||||
}),
|
||||
'Baichuan only': False,
|
||||
'Baichuan port': 5678,
|
||||
'Chimes': dict({
|
||||
'12345678': dict({
|
||||
'channel': 0,
|
||||
@@ -62,6 +64,10 @@
|
||||
0,
|
||||
]),
|
||||
'cmd list': dict({
|
||||
'208': dict({
|
||||
'0': 1,
|
||||
'null': 1,
|
||||
}),
|
||||
'296': dict({
|
||||
'0': 1,
|
||||
'null': 1,
|
||||
|
||||
@@ -141,6 +141,7 @@ async def test_browsing(
|
||||
entry_id = config_entry.entry_id
|
||||
reolink_connect.supported.return_value = 1
|
||||
reolink_connect.model = "Reolink TrackMix PoE"
|
||||
reolink_connect.is_nvr = False
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
assert await hass.config_entries.async_setup(entry_id) is True
|
||||
@@ -333,7 +334,14 @@ async def test_browsing_rec_playback_unsupported(
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test browsing a Reolink camera which does not support playback of recordings."""
|
||||
reolink_connect.supported.return_value = 0
|
||||
|
||||
def test_supported(ch, key):
|
||||
"""Test supported function."""
|
||||
if key == "replay":
|
||||
return False
|
||||
return True
|
||||
|
||||
reolink_connect.supported = test_supported
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -347,6 +355,8 @@ async def test_browsing_rec_playback_unsupported(
|
||||
assert browse.identifier is None
|
||||
assert browse.children == []
|
||||
|
||||
reolink_connect.supported = lambda ch, key: True # Reset supported function
|
||||
|
||||
|
||||
async def test_browsing_errors(
|
||||
hass: HomeAssistant,
|
||||
@@ -354,8 +364,6 @@ async def test_browsing_errors(
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test browsing a Reolink camera errors."""
|
||||
reolink_connect.supported.return_value = 1
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -373,8 +381,6 @@ async def test_browsing_not_loaded(
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test browsing a Reolink camera integration which is not loaded."""
|
||||
reolink_connect.supported.return_value = 1
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -27,13 +27,12 @@ async def setup_component(hass: HomeAssistant) -> None:
|
||||
{"external_url": "https://example.com"},
|
||||
)
|
||||
|
||||
with patch("os.path.isfile", return_value=False):
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert await async_setup_component(
|
||||
hass,
|
||||
DOMAIN,
|
||||
{DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_abort_if_no_configuration(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# serializer version: 1
|
||||
# name: test_fan_status[dmaker.fan.p18][fan.test_fan-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
'Normal',
|
||||
'Nature',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.test_fan',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'xiaomi_miio',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <FanEntityFeature: 63>,
|
||||
'translation_key': 'generic_fan',
|
||||
'unique_id': '123456',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_status[dmaker.fan.p18][fan.test_fan-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'direction': None,
|
||||
'friendly_name': 'test_fan',
|
||||
'oscillating': None,
|
||||
'percentage': None,
|
||||
'percentage_step': 1.0,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'Normal',
|
||||
'Nature',
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 63>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.test_fan',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_status[dmaker.fan.p5][fan.test_fan-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'preset_modes': list([
|
||||
'Normal',
|
||||
'Nature',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.test_fan',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'xiaomi_miio',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <FanEntityFeature: 63>,
|
||||
'translation_key': 'generic_fan',
|
||||
'unique_id': '123456',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_status[dmaker.fan.p5][fan.test_fan-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'direction': None,
|
||||
'friendly_name': 'test_fan',
|
||||
'oscillating': False,
|
||||
'percentage': None,
|
||||
'percentage_step': 1.0,
|
||||
'preset_mode': 'Nature',
|
||||
'preset_modes': list([
|
||||
'Normal',
|
||||
'Nature',
|
||||
]),
|
||||
'supported_features': <FanEntityFeature: 63>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.test_fan',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,130 @@
|
||||
"""The tests for the xiaomi_miio fan component."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from miio.integrations.fan.dmaker.fan import FanStatusP5
|
||||
from miio.integrations.fan.dmaker.fan_miot import FanStatusMiot
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.xiaomi_miio import MODEL_TO_CLASS_MAP
|
||||
from homeassistant.components.xiaomi_miio.const import CONF_FLOW_TYPE, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_MODEL,
|
||||
CONF_TOKEN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import TEST_MAC
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
_MODEL_INFORMATION = {
|
||||
"dmaker.fan.p5": {
|
||||
"patch_class": "homeassistant.components.xiaomi_miio.FanP5",
|
||||
"mock_status": FanStatusP5(
|
||||
{
|
||||
"roll_angle": 60,
|
||||
"beep_sound": False,
|
||||
"child_lock": False,
|
||||
"time_off": 0,
|
||||
"power": False,
|
||||
"light": True,
|
||||
"mode": "nature",
|
||||
"roll_enable": False,
|
||||
"speed": 64,
|
||||
}
|
||||
),
|
||||
},
|
||||
"dmaker.fan.p18": {
|
||||
"patch_class": "homeassistant.components.xiaomi_miio.FanMiot",
|
||||
"mock_status": FanStatusMiot(
|
||||
{
|
||||
"swing_mode_angle": 90,
|
||||
"buzzer": False,
|
||||
"child_lock": False,
|
||||
"power_off_time": 0,
|
||||
"power": False,
|
||||
"light": True,
|
||||
"mode": 0,
|
||||
"swing_mode": False,
|
||||
"fan_speed": 100,
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
name="model_code",
|
||||
params=_MODEL_INFORMATION.keys(),
|
||||
)
|
||||
def get_model_code(request: pytest.FixtureRequest) -> str:
|
||||
"""Parametrize model code."""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_device(model_code: str) -> Generator[MagicMock]:
|
||||
"""Initialize test xiaomi_miio for fan entity."""
|
||||
|
||||
model_information = _MODEL_INFORMATION[model_code]
|
||||
|
||||
mock_fan = MagicMock()
|
||||
mock_fan.status = Mock(return_value=model_information["mock_status"])
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.xiaomi_miio.get_platforms",
|
||||
return_value=[Platform.FAN],
|
||||
),
|
||||
patch(model_information["patch_class"]) as mock_fan_cls,
|
||||
patch.dict(
|
||||
MODEL_TO_CLASS_MAP,
|
||||
{model_code: mock_fan_cls} if model_code in MODEL_TO_CLASS_MAP else {},
|
||||
),
|
||||
):
|
||||
mock_fan_cls.return_value = mock_fan
|
||||
yield mock_fan
|
||||
|
||||
|
||||
async def setup_component(
|
||||
hass: HomeAssistant, model_code: str, entry_title: str
|
||||
) -> MockConfigEntry:
|
||||
"""Set up fan component."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="123456",
|
||||
title=entry_title,
|
||||
data={
|
||||
CONF_FLOW_TYPE: CONF_DEVICE,
|
||||
CONF_HOST: "192.168.1.100",
|
||||
CONF_TOKEN: "12345678901234567890123456789012",
|
||||
CONF_MODEL: model_code,
|
||||
CONF_MAC: TEST_MAC,
|
||||
},
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
async def test_fan_status(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
model_code: str,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test fan status."""
|
||||
|
||||
config_entry = await setup_component(hass, model_code, "test_fan")
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
+4
-2
@@ -382,8 +382,10 @@ def verify_cleanup(
|
||||
# Verify no threads where left behind.
|
||||
threads = frozenset(threading.enumerate()) - threads_before
|
||||
for thread in threads:
|
||||
assert isinstance(thread, threading._DummyThread) or thread.name.startswith(
|
||||
"waitpid-"
|
||||
assert (
|
||||
isinstance(thread, threading._DummyThread)
|
||||
or thread.name.startswith("waitpid-")
|
||||
or "_run_safe_shutdown_loop" in thread.name
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
+4
-44
@@ -85,21 +85,6 @@ async def test_async_enable_logging(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test to ensure logging is migrated to the queue handlers."""
|
||||
config_log_file_pattern = get_test_config_dir("home-assistant.log*")
|
||||
arg_log_file_pattern = "test.log*"
|
||||
|
||||
def cleanup_log_files() -> None:
|
||||
"""Remove all log files."""
|
||||
for f in glob.glob(config_log_file_pattern):
|
||||
os.remove(f)
|
||||
for f in glob.glob(arg_log_file_pattern):
|
||||
os.remove(f)
|
||||
|
||||
# Ensure we start with a clean slate
|
||||
cleanup_log_files()
|
||||
assert len(glob.glob(config_log_file_pattern)) == 0
|
||||
assert len(glob.glob(arg_log_file_pattern)) == 0
|
||||
|
||||
with (
|
||||
patch("logging.getLogger"),
|
||||
patch(
|
||||
@@ -112,8 +97,6 @@ async def test_async_enable_logging(
|
||||
):
|
||||
await bootstrap.async_enable_logging(hass)
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
assert len(glob.glob(config_log_file_pattern)) > 0
|
||||
|
||||
mock_async_activate_log_queue_handler.reset_mock()
|
||||
await bootstrap.async_enable_logging(
|
||||
hass,
|
||||
@@ -121,36 +104,13 @@ async def test_async_enable_logging(
|
||||
log_file="test.log",
|
||||
)
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
assert len(glob.glob(arg_log_file_pattern)) > 0
|
||||
for f in glob.glob("test.log*"):
|
||||
os.remove(f)
|
||||
for f in glob.glob("testing_config/home-assistant.log*"):
|
||||
os.remove(f)
|
||||
|
||||
assert "Error rolling over log file" in caplog.text
|
||||
|
||||
cleanup_log_files()
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {"SUPERVISOR": "1"}),
|
||||
patch("logging.getLogger"),
|
||||
patch(
|
||||
"homeassistant.bootstrap.async_activate_log_queue_handler"
|
||||
) as mock_async_activate_log_queue_handler,
|
||||
):
|
||||
await bootstrap.async_enable_logging(hass)
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
# On Supervisor, the default log file should not be created
|
||||
assert len(glob.glob(config_log_file_pattern)) == 0
|
||||
|
||||
mock_async_activate_log_queue_handler.reset_mock()
|
||||
await bootstrap.async_enable_logging(
|
||||
hass,
|
||||
log_rotate_days=5,
|
||||
log_file="test.log",
|
||||
)
|
||||
mock_async_activate_log_queue_handler.assert_called_once()
|
||||
# Even on Supervisor, the log file should be created if explicitly set
|
||||
assert len(glob.glob(arg_log_file_pattern)) > 0
|
||||
|
||||
cleanup_log_files()
|
||||
|
||||
|
||||
async def test_load_hassio(hass: HomeAssistant) -> None:
|
||||
"""Test that we load the hassio integration when using Supervisor."""
|
||||
|
||||
+47
-32
@@ -255,45 +255,51 @@ async def test_async_add_hass_job_schedule_partial_callback() -> None:
|
||||
partial = functools.partial(ha.callback(job))
|
||||
|
||||
ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(partial))
|
||||
assert len(hass.loop.call_soon.mock_calls) == 1
|
||||
assert len(hass.loop.create_task.mock_calls) == 0
|
||||
assert len(hass.add_job.mock_calls) == 0
|
||||
assert hass.loop.call_soon.call_count == 1
|
||||
assert hass.loop.create_task.call_count == 0
|
||||
assert hass.add_job.call_count == 0
|
||||
|
||||
|
||||
async def test_async_add_hass_job_schedule_corofunction_eager_start() -> None:
|
||||
"""Test that we schedule coroutines and add jobs to the job pool."""
|
||||
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
|
||||
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
|
||||
|
||||
async def job():
|
||||
pass
|
||||
|
||||
with patch(
|
||||
"homeassistant.core.create_eager_task", wraps=create_eager_task
|
||||
) as mock_create_eager_task:
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.core.create_eager_task", wraps=create_eager_task
|
||||
) as mock_create_eager_task,
|
||||
patch.object(loop, "call_soon") as mock_loop_call_soon,
|
||||
):
|
||||
hass_job = ha.HassJob(job)
|
||||
task = ha.HomeAssistant._async_add_hass_job(hass, hass_job)
|
||||
assert len(hass.loop.call_soon.mock_calls) == 0
|
||||
assert len(hass.add_job.mock_calls) == 0
|
||||
assert mock_loop_call_soon.call_count == 0
|
||||
assert hass.add_job.call_count == 0
|
||||
assert mock_create_eager_task.mock_calls
|
||||
await task
|
||||
|
||||
|
||||
async def test_async_add_hass_job_schedule_partial_corofunction_eager_start() -> None:
|
||||
"""Test that we schedule coroutines and add jobs to the job pool."""
|
||||
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
|
||||
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
|
||||
|
||||
async def job():
|
||||
pass
|
||||
|
||||
partial = functools.partial(job)
|
||||
|
||||
with patch(
|
||||
"homeassistant.core.create_eager_task", wraps=create_eager_task
|
||||
) as mock_create_eager_task:
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.core.create_eager_task", wraps=create_eager_task
|
||||
) as mock_create_eager_task,
|
||||
patch.object(loop, "call_soon") as mock_loop_call_soon,
|
||||
):
|
||||
hass_job = ha.HassJob(partial)
|
||||
task = ha.HomeAssistant._async_add_hass_job(hass, hass_job)
|
||||
assert len(hass.loop.call_soon.mock_calls) == 0
|
||||
assert len(hass.add_job.mock_calls) == 0
|
||||
assert mock_loop_call_soon.call_count == 0
|
||||
assert hass.add_job.call_count == 0
|
||||
assert mock_create_eager_task.mock_calls
|
||||
await task
|
||||
|
||||
@@ -306,35 +312,42 @@ async def test_async_add_job_add_hass_threaded_job_to_pool() -> None:
|
||||
pass
|
||||
|
||||
ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(job))
|
||||
assert len(hass.loop.call_soon.mock_calls) == 0
|
||||
assert len(hass.loop.create_task.mock_calls) == 0
|
||||
assert len(hass.loop.run_in_executor.mock_calls) == 2
|
||||
assert hass.loop.call_soon.call_count == 0
|
||||
assert hass.loop.create_task.call_count == 0
|
||||
assert hass.loop.run_in_executor.call_count == 1
|
||||
|
||||
|
||||
async def test_async_create_task_schedule_coroutine() -> None:
|
||||
"""Test that we schedule coroutines and add jobs to the job pool."""
|
||||
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
|
||||
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
|
||||
|
||||
async def job():
|
||||
pass
|
||||
|
||||
ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=False)
|
||||
assert len(hass.loop.call_soon.mock_calls) == 0
|
||||
assert len(hass.loop.create_task.mock_calls) == 1
|
||||
assert len(hass.add_job.mock_calls) == 0
|
||||
with (
|
||||
patch.object(loop, "call_soon") as mock_loop_call_soon,
|
||||
patch.object(loop, "create_task") as mock_loop_create_task,
|
||||
):
|
||||
coro = job()
|
||||
ha.HomeAssistant.async_create_task_internal(hass, coro, eager_start=False)
|
||||
assert mock_loop_call_soon.call_count == 0
|
||||
assert mock_loop_create_task.call_count == 1
|
||||
assert hass.add_job.call_count == 0
|
||||
await coro
|
||||
|
||||
|
||||
async def test_async_create_task_eager_start_schedule_coroutine() -> None:
|
||||
"""Test that we schedule coroutines and add jobs to the job pool."""
|
||||
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
|
||||
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
|
||||
|
||||
async def job():
|
||||
pass
|
||||
|
||||
ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True)
|
||||
# Should create the task directly since 3.12 supports eager_start
|
||||
assert len(hass.loop.create_task.mock_calls) == 0
|
||||
assert len(hass.add_job.mock_calls) == 0
|
||||
with patch.object(loop, "create_task") as mock_loop_create_task:
|
||||
ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True)
|
||||
# Should create the task directly since 3.12 supports eager_start
|
||||
assert mock_loop_create_task.call_count == 0
|
||||
assert hass.add_job.call_count == 0
|
||||
|
||||
|
||||
async def test_async_create_task_schedule_coroutine_with_name() -> None:
|
||||
@@ -344,13 +357,15 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None:
|
||||
async def job():
|
||||
pass
|
||||
|
||||
coro = job()
|
||||
task = ha.HomeAssistant.async_create_task_internal(
|
||||
hass, job(), "named task", eager_start=False
|
||||
hass, coro, "named task", eager_start=False
|
||||
)
|
||||
assert len(hass.loop.call_soon.mock_calls) == 0
|
||||
assert len(hass.loop.create_task.mock_calls) == 1
|
||||
assert len(hass.add_job.mock_calls) == 0
|
||||
assert hass.loop.call_soon.call_count == 0
|
||||
assert hass.loop.create_task.call_count == 1
|
||||
assert hass.add_job.call_count == 0
|
||||
assert "named task" in str(task)
|
||||
await coro
|
||||
|
||||
|
||||
async def test_async_run_eager_hass_job_calls_callback() -> None:
|
||||
|
||||
Reference in New Issue
Block a user