mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Merge pull request #65713 from home-assistant/rc
This commit is contained in:
commit
0f02ae981d
@ -124,6 +124,14 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return RESULT_CONN_ERROR, None
|
||||
|
||||
dev_prop = aftv.device_properties
|
||||
_LOGGER.info(
|
||||
"Android TV at %s: %s = %r, %s = %r",
|
||||
user_input[CONF_HOST],
|
||||
PROP_ETHMAC,
|
||||
dev_prop.get(PROP_ETHMAC),
|
||||
PROP_WIFIMAC,
|
||||
dev_prop.get(PROP_WIFIMAC),
|
||||
)
|
||||
unique_id = format_mac(
|
||||
dev_prop.get(PROP_ETHMAC) or dev_prop.get(PROP_WIFIMAC, "")
|
||||
)
|
||||
|
@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.4.0",
|
||||
"androidtv[async]==0.0.61",
|
||||
"androidtv[async]==0.0.63",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
],
|
||||
"codeowners": ["@JeffLIrion", "@ollo69"],
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Bosch SHC",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
|
||||
"requirements": ["boschshcpy==0.2.28"],
|
||||
"requirements": ["boschshcpy==0.2.29"],
|
||||
"zeroconf": [{ "type": "_http._tcp.local.", "name": "bosch shc*" }],
|
||||
"iot_class": "local_push",
|
||||
"codeowners": ["@tschamm"],
|
||||
|
@ -113,8 +113,9 @@ class ButtonEntity(RestoreEntity):
|
||||
self.async_write_ha_state()
|
||||
await self.async_press()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the button is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if state is not None and state.state is not None:
|
||||
self.__last_pressed = dt_util.parse_datetime(state.state)
|
||||
|
@ -232,7 +232,11 @@ class WebDavCalendarData:
|
||||
new_events.append(new_event)
|
||||
elif _start_of_tomorrow <= start_dt:
|
||||
break
|
||||
vevents = [event.instance.vevent for event in results + new_events]
|
||||
vevents = [
|
||||
event.instance.vevent
|
||||
for event in results + new_events
|
||||
if hasattr(event.instance, "vevent")
|
||||
]
|
||||
|
||||
# dtstart can be a date or datetime depending if the event lasts a
|
||||
# whole day. Convert everything to datetime to be able to sort it
|
||||
|
@ -62,6 +62,9 @@ async def websocket_update_device(hass, connection, msg):
|
||||
msg.pop("type")
|
||||
msg_id = msg.pop("id")
|
||||
|
||||
if "disabled_by" in msg:
|
||||
msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"])
|
||||
|
||||
entry = registry.async_update_device(**msg)
|
||||
|
||||
connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry)))
|
||||
|
@ -7,7 +7,6 @@
|
||||
"cloud",
|
||||
"counter",
|
||||
"dhcp",
|
||||
"diagnostics",
|
||||
"energy",
|
||||
"frontend",
|
||||
"history",
|
||||
|
@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"requirements": ["flux_led==0.28.17"],
|
||||
"requirements": ["flux_led==0.28.20"],
|
||||
"quality_scale": "platinum",
|
||||
"codeowners": ["@icemanch", "@bdraco"],
|
||||
"iot_class": "local_push",
|
||||
|
@ -10,6 +10,7 @@
|
||||
"auth",
|
||||
"config",
|
||||
"device_automation",
|
||||
"diagnostics",
|
||||
"http",
|
||||
"lovelace",
|
||||
"onboarding",
|
||||
|
@ -10,7 +10,6 @@ from aiogithubapi import (
|
||||
GitHubException,
|
||||
GitHubLoginDeviceModel,
|
||||
GitHubLoginOauthModel,
|
||||
GitHubRepositoryModel,
|
||||
)
|
||||
from aiogithubapi.const import OAUTH_USER_LOGIN
|
||||
import voluptuous as vol
|
||||
@ -34,11 +33,12 @@ from .const import (
|
||||
)
|
||||
|
||||
|
||||
async def starred_repositories(hass: HomeAssistant, access_token: str) -> list[str]:
|
||||
"""Return a list of repositories that the user has starred."""
|
||||
async def get_repositories(hass: HomeAssistant, access_token: str) -> list[str]:
|
||||
"""Return a list of repositories that the user owns or has starred."""
|
||||
client = GitHubAPI(token=access_token, session=async_get_clientsession(hass))
|
||||
repositories = set()
|
||||
|
||||
async def _get_starred() -> list[GitHubRepositoryModel] | None:
|
||||
async def _get_starred_repositories() -> None:
|
||||
response = await client.user.starred(**{"params": {"per_page": 100}})
|
||||
if not response.is_last_page:
|
||||
results = await asyncio.gather(
|
||||
@ -54,16 +54,44 @@ async def starred_repositories(hass: HomeAssistant, access_token: str) -> list[s
|
||||
for result in results:
|
||||
response.data.extend(result.data)
|
||||
|
||||
return response.data
|
||||
repositories.update(response.data)
|
||||
|
||||
async def _get_personal_repositories() -> None:
|
||||
response = await client.user.repos(**{"params": {"per_page": 100}})
|
||||
if not response.is_last_page:
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
client.user.repos(
|
||||
**{"params": {"per_page": 100, "page": page_number}},
|
||||
)
|
||||
for page_number in range(
|
||||
response.next_page_number, response.last_page_number + 1
|
||||
)
|
||||
)
|
||||
)
|
||||
for result in results:
|
||||
response.data.extend(result.data)
|
||||
|
||||
repositories.update(response.data)
|
||||
|
||||
try:
|
||||
result = await _get_starred()
|
||||
await asyncio.gather(
|
||||
*(
|
||||
_get_starred_repositories(),
|
||||
_get_personal_repositories(),
|
||||
)
|
||||
)
|
||||
|
||||
except GitHubException:
|
||||
return DEFAULT_REPOSITORIES
|
||||
|
||||
if not result or len(result) == 0:
|
||||
if len(repositories) == 0:
|
||||
return DEFAULT_REPOSITORIES
|
||||
return sorted((repo.full_name for repo in result), key=str.casefold)
|
||||
|
||||
return sorted(
|
||||
(repo.full_name for repo in repositories),
|
||||
key=str.casefold,
|
||||
)
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@ -153,9 +181,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
assert self._login is not None
|
||||
|
||||
if not user_input:
|
||||
repositories = await starred_repositories(
|
||||
self.hass, self._login.access_token
|
||||
)
|
||||
repositories = await get_repositories(self.hass, self._login.access_token)
|
||||
return self.async_show_form(
|
||||
step_id="repositories",
|
||||
data_schema=vol.Schema(
|
||||
@ -205,7 +231,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
configured_repositories: list[str] = self.config_entry.options[
|
||||
CONF_REPOSITORIES
|
||||
]
|
||||
repositories = await starred_repositories(
|
||||
repositories = await get_repositories(
|
||||
self.hass, self.config_entry.data[CONF_ACCESS_TOKEN]
|
||||
)
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "GitHub",
|
||||
"documentation": "https://www.home-assistant.io/integrations/github",
|
||||
"requirements": [
|
||||
"aiogithubapi==22.1.0"
|
||||
"aiogithubapi==22.2.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@timmo001",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "HomematicIP Cloud",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"requirements": ["homematicip==1.0.1"],
|
||||
"requirements": ["homematicip==1.0.2"],
|
||||
"codeowners": [],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_push"
|
||||
|
@ -3,10 +3,11 @@ import logging
|
||||
|
||||
from aiohwenergy import DisabledError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
@ -20,6 +21,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
_LOGGER.debug("__init__ async_setup_entry")
|
||||
|
||||
# Migrate `homewizard_energy` (custom_component) to `homewizard`
|
||||
if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data:
|
||||
# Remove the old config entry ID from the entry data so we don't try this again
|
||||
# on the next setup
|
||||
data = entry.data.copy()
|
||||
old_config_entry_id = data.pop("old_config_entry_id")
|
||||
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Setting up imported homewizard_energy entry %s for the first time as "
|
||||
"homewizard entry %s"
|
||||
),
|
||||
old_config_entry_id,
|
||||
entry.entry_id,
|
||||
)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
for entity in er.async_entries_for_config_entry(ent_reg, old_config_entry_id):
|
||||
_LOGGER.debug("Removing %s", entity.entity_id)
|
||||
ent_reg.async_remove(entity.entity_id)
|
||||
|
||||
_LOGGER.debug("Re-creating %s for the new config entry", entity.entity_id)
|
||||
# We will precreate the entity so that any customizations can be preserved
|
||||
new_entity = ent_reg.async_get_or_create(
|
||||
entity.domain,
|
||||
DOMAIN,
|
||||
entity.unique_id,
|
||||
suggested_object_id=entity.entity_id.split(".")[1],
|
||||
disabled_by=entity.disabled_by,
|
||||
config_entry=entry,
|
||||
original_name=entity.original_name,
|
||||
original_icon=entity.original_icon,
|
||||
)
|
||||
_LOGGER.debug("Re-created %s", new_entity.entity_id)
|
||||
|
||||
# If there are customizations on the old entity, apply them to the new one
|
||||
if entity.name or entity.icon:
|
||||
ent_reg.async_update_entity(
|
||||
new_entity.entity_id, name=entity.name, icon=entity.icon
|
||||
)
|
||||
|
||||
# Remove the old config entry and now the entry is fully migrated
|
||||
hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id))
|
||||
|
||||
# Create coordinator
|
||||
coordinator = Coordinator(hass, entry.data[CONF_IP_ADDRESS])
|
||||
try:
|
||||
|
@ -28,6 +28,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize the HomeWizard config flow."""
|
||||
self.config: dict[str, str | int] = {}
|
||||
|
||||
async def async_step_import(self, import_config: dict) -> FlowResult:
|
||||
"""Handle a flow initiated by older `homewizard_energy` component."""
|
||||
_LOGGER.debug("config_flow async_step_import")
|
||||
|
||||
self.hass.components.persistent_notification.async_create(
|
||||
(
|
||||
"The custom integration of HomeWizard Energy has been migrated to core. "
|
||||
"You can safely remove the custom integration from the custom_integrations folder."
|
||||
),
|
||||
"HomeWizard Energy",
|
||||
f"homewizard_energy_to_{DOMAIN}",
|
||||
)
|
||||
|
||||
return await self.async_step_user({CONF_IP_ADDRESS: import_config["host"]})
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
@ -59,12 +74,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
data: dict[str, str] = {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
|
||||
|
||||
if self.source == config_entries.SOURCE_IMPORT:
|
||||
old_config_entry_id = self.context["old_config_entry_id"]
|
||||
assert self.hass.config_entries.async_get_entry(old_config_entry_id)
|
||||
data["old_config_entry_id"] = old_config_entry_id
|
||||
|
||||
# Add entry
|
||||
return self.async_create_entry(
|
||||
title=f"{device_info[CONF_PRODUCT_NAME]} ({device_info[CONF_SERIAL]})",
|
||||
data={
|
||||
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
|
||||
},
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
|
@ -6,6 +6,7 @@ import logging
|
||||
import sqlalchemy
|
||||
from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text
|
||||
from sqlalchemy.exc import (
|
||||
DatabaseError,
|
||||
InternalError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
@ -68,20 +69,18 @@ def schema_is_current(current_version):
|
||||
|
||||
def migrate_schema(instance, current_version):
|
||||
"""Check if the schema needs to be upgraded."""
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
_LOGGER.warning(
|
||||
"Database is about to upgrade. Schema version: %s", current_version
|
||||
)
|
||||
for version in range(current_version, SCHEMA_VERSION):
|
||||
new_version = version + 1
|
||||
_LOGGER.info("Upgrading recorder db schema to version %s", new_version)
|
||||
_apply_update(instance, session, new_version, current_version)
|
||||
_LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version)
|
||||
for version in range(current_version, SCHEMA_VERSION):
|
||||
new_version = version + 1
|
||||
_LOGGER.info("Upgrading recorder db schema to version %s", new_version)
|
||||
_apply_update(instance, new_version, current_version)
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
session.add(SchemaChanges(schema_version=new_version))
|
||||
|
||||
_LOGGER.info("Upgrade to version %s done", new_version)
|
||||
_LOGGER.info("Upgrade to version %s done", new_version)
|
||||
|
||||
|
||||
def _create_index(connection, table_name, index_name):
|
||||
def _create_index(instance, table_name, index_name):
|
||||
"""Create an index for the specified table.
|
||||
|
||||
The index name should match the name given for the index
|
||||
@ -103,8 +102,10 @@ def _create_index(connection, table_name, index_name):
|
||||
index_name,
|
||||
)
|
||||
try:
|
||||
index.create(connection)
|
||||
except (InternalError, ProgrammingError, OperationalError) as err:
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
index.create(connection)
|
||||
except (InternalError, OperationalError, ProgrammingError) as err:
|
||||
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
|
||||
_LOGGER.warning(
|
||||
"Index %s already exists on %s, continuing", index_name, table_name
|
||||
@ -113,7 +114,7 @@ def _create_index(connection, table_name, index_name):
|
||||
_LOGGER.debug("Finished creating %s", index_name)
|
||||
|
||||
|
||||
def _drop_index(connection, table_name, index_name):
|
||||
def _drop_index(instance, table_name, index_name):
|
||||
"""Drop an index from a specified table.
|
||||
|
||||
There is no universal way to do something like `DROP INDEX IF EXISTS`
|
||||
@ -129,7 +130,9 @@ def _drop_index(connection, table_name, index_name):
|
||||
|
||||
# Engines like DB2/Oracle
|
||||
try:
|
||||
connection.execute(text(f"DROP INDEX {index_name}"))
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(text(f"DROP INDEX {index_name}"))
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
@ -138,13 +141,15 @@ def _drop_index(connection, table_name, index_name):
|
||||
# Engines like SQLite, SQL Server
|
||||
if not success:
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"DROP INDEX {table}.{index}".format(
|
||||
index=index_name, table=table_name
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
"DROP INDEX {table}.{index}".format(
|
||||
index=index_name, table=table_name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
@ -153,13 +158,15 @@ def _drop_index(connection, table_name, index_name):
|
||||
if not success:
|
||||
# Engines like MySQL, MS Access
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"DROP INDEX {index} ON {table}".format(
|
||||
index=index_name, table=table_name
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
"DROP INDEX {index} ON {table}".format(
|
||||
index=index_name, table=table_name
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
except SQLAlchemyError:
|
||||
pass
|
||||
else:
|
||||
@ -184,7 +191,7 @@ def _drop_index(connection, table_name, index_name):
|
||||
)
|
||||
|
||||
|
||||
def _add_columns(connection, table_name, columns_def):
|
||||
def _add_columns(instance, table_name, columns_def):
|
||||
"""Add columns to a table."""
|
||||
_LOGGER.warning(
|
||||
"Adding columns %s to table %s. Note: this can take several "
|
||||
@ -197,29 +204,33 @@ def _add_columns(connection, table_name, columns_def):
|
||||
columns_def = [f"ADD {col_def}" for col_def in columns_def]
|
||||
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {columns_def}".format(
|
||||
table=table_name, columns_def=", ".join(columns_def)
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {columns_def}".format(
|
||||
table=table_name, columns_def=", ".join(columns_def)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
except (InternalError, OperationalError):
|
||||
return
|
||||
except (InternalError, OperationalError, ProgrammingError):
|
||||
# Some engines support adding all columns at once,
|
||||
# this error is when they don't
|
||||
_LOGGER.info("Unable to use quick column add. Adding 1 by 1")
|
||||
|
||||
for column_def in columns_def:
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {column_def}".format(
|
||||
table=table_name, column_def=column_def
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {column_def}".format(
|
||||
table=table_name, column_def=column_def
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
except (InternalError, OperationalError) as err:
|
||||
except (InternalError, OperationalError, ProgrammingError) as err:
|
||||
raise_if_exception_missing_str(err, ["already exists", "duplicate"])
|
||||
_LOGGER.warning(
|
||||
"Column %s already exists on %s, continuing",
|
||||
@ -228,7 +239,7 @@ def _add_columns(connection, table_name, columns_def):
|
||||
)
|
||||
|
||||
|
||||
def _modify_columns(connection, engine, table_name, columns_def):
|
||||
def _modify_columns(instance, engine, table_name, columns_def):
|
||||
"""Modify columns in a table."""
|
||||
if engine.dialect.name == "sqlite":
|
||||
_LOGGER.debug(
|
||||
@ -261,33 +272,37 @@ def _modify_columns(connection, engine, table_name, columns_def):
|
||||
columns_def = [f"MODIFY {col_def}" for col_def in columns_def]
|
||||
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {columns_def}".format(
|
||||
table=table_name, columns_def=", ".join(columns_def)
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {columns_def}".format(
|
||||
table=table_name, columns_def=", ".join(columns_def)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
return
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.info("Unable to use quick column modify. Modifying 1 by 1")
|
||||
|
||||
for column_def in columns_def:
|
||||
try:
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {column_def}".format(
|
||||
table=table_name, column_def=column_def
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
text(
|
||||
"ALTER TABLE {table} {column_def}".format(
|
||||
table=table_name, column_def=column_def
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not modify column %s in table %s", column_def, table_name
|
||||
)
|
||||
|
||||
|
||||
def _update_states_table_with_foreign_key_options(connection, engine):
|
||||
def _update_states_table_with_foreign_key_options(instance, engine):
|
||||
"""Add the options to foreign key constraints."""
|
||||
inspector = sqlalchemy.inspect(engine)
|
||||
alters = []
|
||||
@ -316,17 +331,19 @@ def _update_states_table_with_foreign_key_options(connection, engine):
|
||||
|
||||
for alter in alters:
|
||||
try:
|
||||
connection.execute(DropConstraint(alter["old_fk"]))
|
||||
for fkc in states_key_constraints:
|
||||
if fkc.column_keys == alter["columns"]:
|
||||
connection.execute(AddConstraint(fkc))
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(DropConstraint(alter["old_fk"]))
|
||||
for fkc in states_key_constraints:
|
||||
if fkc.column_keys == alter["columns"]:
|
||||
connection.execute(AddConstraint(fkc))
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not update foreign options in %s table", TABLE_STATES
|
||||
)
|
||||
|
||||
|
||||
def _drop_foreign_key_constraints(connection, engine, table, columns):
|
||||
def _drop_foreign_key_constraints(instance, engine, table, columns):
|
||||
"""Drop foreign key constraints for a table on specific columns."""
|
||||
inspector = sqlalchemy.inspect(engine)
|
||||
drops = []
|
||||
@ -345,7 +362,9 @@ def _drop_foreign_key_constraints(connection, engine, table, columns):
|
||||
|
||||
for drop in drops:
|
||||
try:
|
||||
connection.execute(DropConstraint(drop))
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(DropConstraint(drop))
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception(
|
||||
"Could not drop foreign constraints in %s table on %s",
|
||||
@ -354,17 +373,16 @@ def _drop_foreign_key_constraints(connection, engine, table, columns):
|
||||
)
|
||||
|
||||
|
||||
def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
def _apply_update(instance, new_version, old_version): # noqa: C901
|
||||
"""Perform operations to bring schema up to date."""
|
||||
engine = instance.engine
|
||||
connection = session.connection()
|
||||
if new_version == 1:
|
||||
_create_index(connection, "events", "ix_events_time_fired")
|
||||
_create_index(instance, "events", "ix_events_time_fired")
|
||||
elif new_version == 2:
|
||||
# Create compound start/end index for recorder_runs
|
||||
_create_index(connection, "recorder_runs", "ix_recorder_runs_start_end")
|
||||
_create_index(instance, "recorder_runs", "ix_recorder_runs_start_end")
|
||||
# Create indexes for states
|
||||
_create_index(connection, "states", "ix_states_last_updated")
|
||||
_create_index(instance, "states", "ix_states_last_updated")
|
||||
elif new_version == 3:
|
||||
# There used to be a new index here, but it was removed in version 4.
|
||||
pass
|
||||
@ -374,41 +392,41 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
|
||||
if old_version == 3:
|
||||
# Remove index that was added in version 3
|
||||
_drop_index(connection, "states", "ix_states_created_domain")
|
||||
_drop_index(instance, "states", "ix_states_created_domain")
|
||||
if old_version == 2:
|
||||
# Remove index that was added in version 2
|
||||
_drop_index(connection, "states", "ix_states_entity_id_created")
|
||||
_drop_index(instance, "states", "ix_states_entity_id_created")
|
||||
|
||||
# Remove indexes that were added in version 0
|
||||
_drop_index(connection, "states", "states__state_changes")
|
||||
_drop_index(connection, "states", "states__significant_changes")
|
||||
_drop_index(connection, "states", "ix_states_entity_id_created")
|
||||
_drop_index(instance, "states", "states__state_changes")
|
||||
_drop_index(instance, "states", "states__significant_changes")
|
||||
_drop_index(instance, "states", "ix_states_entity_id_created")
|
||||
|
||||
_create_index(connection, "states", "ix_states_entity_id_last_updated")
|
||||
_create_index(instance, "states", "ix_states_entity_id_last_updated")
|
||||
elif new_version == 5:
|
||||
# Create supporting index for States.event_id foreign key
|
||||
_create_index(connection, "states", "ix_states_event_id")
|
||||
_create_index(instance, "states", "ix_states_event_id")
|
||||
elif new_version == 6:
|
||||
_add_columns(
|
||||
session,
|
||||
instance,
|
||||
"events",
|
||||
["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"],
|
||||
)
|
||||
_create_index(connection, "events", "ix_events_context_id")
|
||||
_create_index(connection, "events", "ix_events_context_user_id")
|
||||
_create_index(instance, "events", "ix_events_context_id")
|
||||
_create_index(instance, "events", "ix_events_context_user_id")
|
||||
_add_columns(
|
||||
connection,
|
||||
instance,
|
||||
"states",
|
||||
["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"],
|
||||
)
|
||||
_create_index(connection, "states", "ix_states_context_id")
|
||||
_create_index(connection, "states", "ix_states_context_user_id")
|
||||
_create_index(instance, "states", "ix_states_context_id")
|
||||
_create_index(instance, "states", "ix_states_context_user_id")
|
||||
elif new_version == 7:
|
||||
_create_index(connection, "states", "ix_states_entity_id")
|
||||
_create_index(instance, "states", "ix_states_entity_id")
|
||||
elif new_version == 8:
|
||||
_add_columns(connection, "events", ["context_parent_id CHARACTER(36)"])
|
||||
_add_columns(connection, "states", ["old_state_id INTEGER"])
|
||||
_create_index(connection, "events", "ix_events_context_parent_id")
|
||||
_add_columns(instance, "events", ["context_parent_id CHARACTER(36)"])
|
||||
_add_columns(instance, "states", ["old_state_id INTEGER"])
|
||||
_create_index(instance, "events", "ix_events_context_parent_id")
|
||||
elif new_version == 9:
|
||||
# We now get the context from events with a join
|
||||
# since its always there on state_changed events
|
||||
@ -418,36 +436,36 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
# and we would have to move to something like
|
||||
# sqlalchemy alembic to make that work
|
||||
#
|
||||
_drop_index(connection, "states", "ix_states_context_id")
|
||||
_drop_index(connection, "states", "ix_states_context_user_id")
|
||||
_drop_index(instance, "states", "ix_states_context_id")
|
||||
_drop_index(instance, "states", "ix_states_context_user_id")
|
||||
# This index won't be there if they were not running
|
||||
# nightly but we don't treat that as a critical issue
|
||||
_drop_index(connection, "states", "ix_states_context_parent_id")
|
||||
_drop_index(instance, "states", "ix_states_context_parent_id")
|
||||
# Redundant keys on composite index:
|
||||
# We already have ix_states_entity_id_last_updated
|
||||
_drop_index(connection, "states", "ix_states_entity_id")
|
||||
_create_index(connection, "events", "ix_events_event_type_time_fired")
|
||||
_drop_index(connection, "events", "ix_events_event_type")
|
||||
_drop_index(instance, "states", "ix_states_entity_id")
|
||||
_create_index(instance, "events", "ix_events_event_type_time_fired")
|
||||
_drop_index(instance, "events", "ix_events_event_type")
|
||||
elif new_version == 10:
|
||||
# Now done in step 11
|
||||
pass
|
||||
elif new_version == 11:
|
||||
_create_index(connection, "states", "ix_states_old_state_id")
|
||||
_update_states_table_with_foreign_key_options(connection, engine)
|
||||
_create_index(instance, "states", "ix_states_old_state_id")
|
||||
_update_states_table_with_foreign_key_options(instance, engine)
|
||||
elif new_version == 12:
|
||||
if engine.dialect.name == "mysql":
|
||||
_modify_columns(connection, engine, "events", ["event_data LONGTEXT"])
|
||||
_modify_columns(connection, engine, "states", ["attributes LONGTEXT"])
|
||||
_modify_columns(instance, engine, "events", ["event_data LONGTEXT"])
|
||||
_modify_columns(instance, engine, "states", ["attributes LONGTEXT"])
|
||||
elif new_version == 13:
|
||||
if engine.dialect.name == "mysql":
|
||||
_modify_columns(
|
||||
connection,
|
||||
instance,
|
||||
engine,
|
||||
"events",
|
||||
["time_fired DATETIME(6)", "created DATETIME(6)"],
|
||||
)
|
||||
_modify_columns(
|
||||
connection,
|
||||
instance,
|
||||
engine,
|
||||
"states",
|
||||
[
|
||||
@ -457,14 +475,12 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
],
|
||||
)
|
||||
elif new_version == 14:
|
||||
_modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"])
|
||||
_modify_columns(instance, engine, "events", ["event_type VARCHAR(64)"])
|
||||
elif new_version == 15:
|
||||
# This dropped the statistics table, done again in version 18.
|
||||
pass
|
||||
elif new_version == 16:
|
||||
_drop_foreign_key_constraints(
|
||||
connection, engine, TABLE_STATES, ["old_state_id"]
|
||||
)
|
||||
_drop_foreign_key_constraints(instance, engine, TABLE_STATES, ["old_state_id"])
|
||||
elif new_version == 17:
|
||||
# This dropped the statistics table, done again in version 18.
|
||||
pass
|
||||
@ -489,12 +505,13 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
elif new_version == 19:
|
||||
# This adds the statistic runs table, insert a fake run to prevent duplicating
|
||||
# statistics.
|
||||
session.add(StatisticsRuns(start=get_start_time()))
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
session.add(StatisticsRuns(start=get_start_time()))
|
||||
elif new_version == 20:
|
||||
# This changed the precision of statistics from float to double
|
||||
if engine.dialect.name in ["mysql", "postgresql"]:
|
||||
_modify_columns(
|
||||
connection,
|
||||
instance,
|
||||
engine,
|
||||
"statistics",
|
||||
[
|
||||
@ -516,14 +533,16 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
table,
|
||||
)
|
||||
with contextlib.suppress(SQLAlchemyError):
|
||||
connection.execute(
|
||||
# Using LOCK=EXCLUSIVE to prevent the database from corrupting
|
||||
# https://github.com/home-assistant/core/issues/56104
|
||||
text(
|
||||
f"ALTER TABLE {table} CONVERT TO "
|
||||
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci LOCK=EXCLUSIVE"
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
connection = session.connection()
|
||||
connection.execute(
|
||||
# Using LOCK=EXCLUSIVE to prevent the database from corrupting
|
||||
# https://github.com/home-assistant/core/issues/56104
|
||||
text(
|
||||
f"ALTER TABLE {table} CONVERT TO "
|
||||
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci LOCK=EXCLUSIVE"
|
||||
)
|
||||
)
|
||||
)
|
||||
elif new_version == 22:
|
||||
# Recreate the all statistics tables for Oracle DB with Identity columns
|
||||
#
|
||||
@ -549,60 +568,76 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901
|
||||
# Block 5-minute statistics for one hour from the last run, or it will overlap
|
||||
# with existing hourly statistics. Don't block on a database with no existing
|
||||
# statistics.
|
||||
if session.query(Statistics.id).count() and (
|
||||
last_run_string := session.query(func.max(StatisticsRuns.start)).scalar()
|
||||
):
|
||||
last_run_start_time = process_timestamp(last_run_string)
|
||||
if last_run_start_time:
|
||||
fake_start_time = last_run_start_time + timedelta(minutes=5)
|
||||
while fake_start_time < last_run_start_time + timedelta(hours=1):
|
||||
session.add(StatisticsRuns(start=fake_start_time))
|
||||
fake_start_time += timedelta(minutes=5)
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
if session.query(Statistics.id).count() and (
|
||||
last_run_string := session.query(
|
||||
func.max(StatisticsRuns.start)
|
||||
).scalar()
|
||||
):
|
||||
last_run_start_time = process_timestamp(last_run_string)
|
||||
if last_run_start_time:
|
||||
fake_start_time = last_run_start_time + timedelta(minutes=5)
|
||||
while fake_start_time < last_run_start_time + timedelta(hours=1):
|
||||
session.add(StatisticsRuns(start=fake_start_time))
|
||||
fake_start_time += timedelta(minutes=5)
|
||||
|
||||
# When querying the database, be careful to only explicitly query for columns
|
||||
# which were present in schema version 21. If querying the table, SQLAlchemy
|
||||
# will refer to future columns.
|
||||
for sum_statistic in session.query(StatisticsMeta.id).filter_by(has_sum=true()):
|
||||
last_statistic = (
|
||||
session.query(
|
||||
Statistics.start,
|
||||
Statistics.last_reset,
|
||||
Statistics.state,
|
||||
Statistics.sum,
|
||||
)
|
||||
.filter_by(metadata_id=sum_statistic.id)
|
||||
.order_by(Statistics.start.desc())
|
||||
.first()
|
||||
)
|
||||
if last_statistic:
|
||||
session.add(
|
||||
StatisticsShortTerm(
|
||||
metadata_id=sum_statistic.id,
|
||||
start=last_statistic.start,
|
||||
last_reset=last_statistic.last_reset,
|
||||
state=last_statistic.state,
|
||||
sum=last_statistic.sum,
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
for sum_statistic in session.query(StatisticsMeta.id).filter_by(
|
||||
has_sum=true()
|
||||
):
|
||||
last_statistic = (
|
||||
session.query(
|
||||
Statistics.start,
|
||||
Statistics.last_reset,
|
||||
Statistics.state,
|
||||
Statistics.sum,
|
||||
)
|
||||
.filter_by(metadata_id=sum_statistic.id)
|
||||
.order_by(Statistics.start.desc())
|
||||
.first()
|
||||
)
|
||||
if last_statistic:
|
||||
session.add(
|
||||
StatisticsShortTerm(
|
||||
metadata_id=sum_statistic.id,
|
||||
start=last_statistic.start,
|
||||
last_reset=last_statistic.last_reset,
|
||||
state=last_statistic.state,
|
||||
sum=last_statistic.sum,
|
||||
)
|
||||
)
|
||||
elif new_version == 23:
|
||||
# Add name column to StatisticsMeta
|
||||
_add_columns(session, "statistics_meta", ["name VARCHAR(255)"])
|
||||
_add_columns(instance, "statistics_meta", ["name VARCHAR(255)"])
|
||||
elif new_version == 24:
|
||||
# Delete duplicated statistics
|
||||
delete_duplicates(instance, session)
|
||||
# Recreate statistics indices to block duplicated statistics
|
||||
_drop_index(connection, "statistics", "ix_statistics_statistic_id_start")
|
||||
_create_index(connection, "statistics", "ix_statistics_statistic_id_start")
|
||||
_drop_index(instance, "statistics", "ix_statistics_statistic_id_start")
|
||||
_drop_index(
|
||||
connection,
|
||||
"statistics_short_term",
|
||||
"ix_statistics_short_term_statistic_id_start",
|
||||
)
|
||||
_create_index(
|
||||
connection,
|
||||
instance,
|
||||
"statistics_short_term",
|
||||
"ix_statistics_short_term_statistic_id_start",
|
||||
)
|
||||
try:
|
||||
_create_index(instance, "statistics", "ix_statistics_statistic_id_start")
|
||||
_create_index(
|
||||
instance,
|
||||
"statistics_short_term",
|
||||
"ix_statistics_short_term_statistic_id_start",
|
||||
)
|
||||
except DatabaseError:
|
||||
# There may be duplicated statistics entries, delete duplicated statistics
|
||||
# and try again
|
||||
with session_scope(session=instance.get_session()) as session:
|
||||
delete_duplicates(instance, session)
|
||||
_create_index(instance, "statistics", "ix_statistics_statistic_id_start")
|
||||
_create_index(
|
||||
instance,
|
||||
"statistics_short_term",
|
||||
"ix_statistics_short_term_statistic_id_start",
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f"No schema migration defined for version {new_version}")
|
||||
|
@ -119,8 +119,6 @@ QUERY_STATISTIC_META_ID = [
|
||||
StatisticsMeta.statistic_id,
|
||||
]
|
||||
|
||||
MAX_DUPLICATES = 1000000
|
||||
|
||||
STATISTICS_BAKERY = "recorder_statistics_bakery"
|
||||
STATISTICS_META_BAKERY = "recorder_statistics_meta_bakery"
|
||||
STATISTICS_SHORT_TERM_BAKERY = "recorder_statistics_short_term_bakery"
|
||||
@ -351,8 +349,6 @@ def _delete_duplicates_from_table(
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
total_deleted_rows += deleted_rows
|
||||
if total_deleted_rows >= MAX_DUPLICATES:
|
||||
break
|
||||
return (total_deleted_rows, all_non_identical_duplicates)
|
||||
|
||||
|
||||
@ -389,13 +385,6 @@ def delete_duplicates(instance: Recorder, session: scoped_session) -> None:
|
||||
backup_path,
|
||||
)
|
||||
|
||||
if deleted_statistics_rows >= MAX_DUPLICATES:
|
||||
_LOGGER.warning(
|
||||
"Found more than %s duplicated statistic rows, please report at "
|
||||
'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+',
|
||||
MAX_DUPLICATES - 1,
|
||||
)
|
||||
|
||||
deleted_short_term_statistics_rows, _ = _delete_duplicates_from_table(
|
||||
session, StatisticsShortTerm
|
||||
)
|
||||
|
@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/renault",
|
||||
"requirements": [
|
||||
"renault-api==0.1.7"
|
||||
"renault-api==0.1.8"
|
||||
],
|
||||
"codeowners": [
|
||||
"@epenet"
|
||||
|
@ -113,8 +113,9 @@ class Scene(RestoreEntity):
|
||||
self.async_write_ha_state()
|
||||
await self.async_activate(**kwargs)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when the button is added to hass."""
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Call when the scene is added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if state is not None and state.state is not None:
|
||||
self.__last_activated = state.state
|
||||
|
@ -157,7 +157,7 @@ def _async_device_as_dict(hass: HomeAssistant, device: TuyaDevice) -> dict[str,
|
||||
state = hass.states.get(entity_entry.entity_id)
|
||||
state_dict = None
|
||||
if state:
|
||||
state_dict = state.as_dict()
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Redact the `entity_picture` attribute as it contains a token.
|
||||
if "entity_picture" in state_dict["attributes"]:
|
||||
|
@ -123,7 +123,7 @@ class ZwaveDimmer(ZWaveDeviceEntity, LightEntity):
|
||||
self._state = None
|
||||
self._color_mode = None
|
||||
self._supported_color_modes = set()
|
||||
self._supported_features = None
|
||||
self._supported_features = 0
|
||||
self._delay = delay
|
||||
self._refresh_value = refresh
|
||||
self._zw098 = None
|
||||
|
@ -99,7 +99,16 @@ async def async_validate_condition_config(
|
||||
|
||||
# We return early if the config entry for this device is not ready because we can't
|
||||
# validate the value without knowing the state of the device
|
||||
if async_is_device_config_entry_not_loaded(hass, config[CONF_DEVICE_ID]):
|
||||
try:
|
||||
device_config_entry_not_loaded = async_is_device_config_entry_not_loaded(
|
||||
hass, config[CONF_DEVICE_ID]
|
||||
)
|
||||
except ValueError as err:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Device {config[CONF_DEVICE_ID]} not found"
|
||||
) from err
|
||||
|
||||
if device_config_entry_not_loaded:
|
||||
return config
|
||||
|
||||
if config[CONF_TYPE] == VALUE_TYPE:
|
||||
|
@ -217,7 +217,16 @@ async def async_validate_trigger_config(
|
||||
|
||||
# We return early if the config entry for this device is not ready because we can't
|
||||
# validate the value without knowing the state of the device
|
||||
if async_is_device_config_entry_not_loaded(hass, config[CONF_DEVICE_ID]):
|
||||
try:
|
||||
device_config_entry_not_loaded = async_is_device_config_entry_not_loaded(
|
||||
hass, config[CONF_DEVICE_ID]
|
||||
)
|
||||
except ValueError as err:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Device {config[CONF_DEVICE_ID]} not found"
|
||||
) from err
|
||||
|
||||
if device_config_entry_not_loaded:
|
||||
return config
|
||||
|
||||
trigger_type = config[CONF_TYPE]
|
||||
|
@ -298,7 +298,8 @@ def async_is_device_config_entry_not_loaded(
|
||||
"""Return whether device's config entries are not loaded."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
device = dev_reg.async_get(device_id)
|
||||
assert device
|
||||
if device is None:
|
||||
raise ValueError(f"Device {device_id} not found")
|
||||
return any(
|
||||
(entry := hass.config_entries.async_get_entry(entry_id))
|
||||
and entry.state != ConfigEntryState.LOADED
|
||||
|
@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 2
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
@ -41,8 +41,11 @@ async def async_get_system_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
|
||||
# Determine installation type on current data
|
||||
if info_object["docker"]:
|
||||
if info_object["user"] == "root":
|
||||
if info_object["user"] == "root" and os.path.isfile("/OFFICIAL_IMAGE"):
|
||||
info_object["installation_type"] = "Home Assistant Container"
|
||||
else:
|
||||
info_object["installation_type"] = "Unsupported Third Party Container"
|
||||
|
||||
elif is_virtual_env():
|
||||
info_object["installation_type"] = "Home Assistant Core"
|
||||
|
||||
|
@ -531,13 +531,33 @@ def color_temperature_to_rgb(
|
||||
def color_temperature_to_rgbww(
|
||||
temperature: int, brightness: int, min_mireds: int, max_mireds: int
|
||||
) -> tuple[int, int, int, int, int]:
|
||||
"""Convert color temperature to rgbcw."""
|
||||
"""Convert color temperature in mireds to rgbcw."""
|
||||
mired_range = max_mireds - min_mireds
|
||||
warm = ((max_mireds - temperature) / mired_range) * brightness
|
||||
cold = brightness - warm
|
||||
cold = ((max_mireds - temperature) / mired_range) * brightness
|
||||
warm = brightness - cold
|
||||
return (0, 0, 0, round(cold), round(warm))
|
||||
|
||||
|
||||
def rgbww_to_color_temperature(
|
||||
rgbww: tuple[int, int, int, int, int], min_mireds: int, max_mireds: int
|
||||
) -> tuple[int, int]:
|
||||
"""Convert rgbcw to color temperature in mireds."""
|
||||
_, _, _, cold, warm = rgbww
|
||||
return while_levels_to_color_temperature(cold, warm, min_mireds, max_mireds)
|
||||
|
||||
|
||||
def while_levels_to_color_temperature(
|
||||
cold: int, warm: int, min_mireds: int, max_mireds: int
|
||||
) -> tuple[int, int]:
|
||||
"""Convert whites to color temperature in mireds."""
|
||||
brightness = warm / 255 + cold / 255
|
||||
if brightness == 0:
|
||||
return (max_mireds, 0)
|
||||
return round(
|
||||
((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds
|
||||
), min(255, round(brightness * 255))
|
||||
|
||||
|
||||
def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float:
|
||||
"""
|
||||
Clamp the given color component value between the given min and max values.
|
||||
|
@ -175,7 +175,7 @@ aioflo==2021.11.0
|
||||
aioftp==0.12.0
|
||||
|
||||
# homeassistant.components.github
|
||||
aiogithubapi==22.1.0
|
||||
aiogithubapi==22.2.0
|
||||
|
||||
# homeassistant.components.guardian
|
||||
aioguardian==2021.11.0
|
||||
@ -311,7 +311,7 @@ ambiclimate==0.2.1
|
||||
amcrest==1.9.3
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.61
|
||||
androidtv[async]==0.0.63
|
||||
|
||||
# homeassistant.components.anel_pwrctrl
|
||||
anel_pwrctrl-homeassistant==0.0.1.dev2
|
||||
@ -438,7 +438,7 @@ blockchain==1.4.4
|
||||
bond-api==0.1.16
|
||||
|
||||
# homeassistant.components.bosch_shc
|
||||
boschshcpy==0.2.28
|
||||
boschshcpy==0.2.29
|
||||
|
||||
# homeassistant.components.amazon_polly
|
||||
# homeassistant.components.route53
|
||||
@ -681,7 +681,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.28.17
|
||||
flux_led==0.28.20
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -851,7 +851,7 @@ homeassistant-pyozw==0.1.10
|
||||
homeconnect==0.6.3
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==1.0.1
|
||||
homematicip==1.0.2
|
||||
|
||||
# homeassistant.components.home_plus_control
|
||||
homepluscontrol==0.0.5
|
||||
@ -2087,7 +2087,7 @@ raspyrfm-client==1.2.8
|
||||
regenmaschine==2022.01.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.7
|
||||
renault-api==0.1.8
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
|
@ -125,7 +125,7 @@ aioesphomeapi==10.8.1
|
||||
aioflo==2021.11.0
|
||||
|
||||
# homeassistant.components.github
|
||||
aiogithubapi==22.1.0
|
||||
aiogithubapi==22.2.0
|
||||
|
||||
# homeassistant.components.guardian
|
||||
aioguardian==2021.11.0
|
||||
@ -237,7 +237,7 @@ amberelectric==1.0.3
|
||||
ambiclimate==0.2.1
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.61
|
||||
androidtv[async]==0.0.63
|
||||
|
||||
# homeassistant.components.apns
|
||||
apns2==0.3.0
|
||||
@ -291,7 +291,7 @@ blinkpy==0.18.0
|
||||
bond-api==0.1.16
|
||||
|
||||
# homeassistant.components.bosch_shc
|
||||
boschshcpy==0.2.28
|
||||
boschshcpy==0.2.29
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
bravia-tv==1.0.11
|
||||
@ -427,7 +427,7 @@ fjaraskupan==1.0.2
|
||||
flipr-api==1.4.1
|
||||
|
||||
# homeassistant.components.flux_led
|
||||
flux_led==0.28.17
|
||||
flux_led==0.28.20
|
||||
|
||||
# homeassistant.components.homekit
|
||||
fnvhash==0.1.0
|
||||
@ -552,7 +552,7 @@ homeassistant-pyozw==0.1.10
|
||||
homeconnect==0.6.3
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==1.0.1
|
||||
homematicip==1.0.2
|
||||
|
||||
# homeassistant.components.home_plus_control
|
||||
homepluscontrol==0.0.5
|
||||
@ -1282,7 +1282,7 @@ rachiopy==1.0.3
|
||||
regenmaschine==2022.01.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.7
|
||||
renault-api==0.1.8
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
|
@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = homeassistant
|
||||
version = 2022.2.1
|
||||
version = 2022.2.2
|
||||
author = The Home Assistant Authors
|
||||
author_email = hello@home-assistant.io
|
||||
license = Apache-2.0
|
||||
|
@ -185,3 +185,15 @@ PATCH_ANDROIDTV_UPDATE_EXCEPTION = patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.update",
|
||||
side_effect=ZeroDivisionError,
|
||||
)
|
||||
|
||||
PATCH_DEVICE_PROPERTIES = patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.get_device_properties",
|
||||
return_value={
|
||||
"manufacturer": "a",
|
||||
"model": "b",
|
||||
"serialno": "c",
|
||||
"sw_version": "d",
|
||||
"wifimac": "ab:cd:ef:gh:ij:kl",
|
||||
"ethmac": None,
|
||||
},
|
||||
)
|
||||
|
@ -157,8 +157,10 @@ async def test_setup_with_properties(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(response)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
||||
@ -188,8 +190,9 @@ async def test_reconnect(hass, caplog, config):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
@ -256,8 +259,10 @@ async def test_adb_shell_returns_none(hass, config):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
@ -284,8 +289,10 @@ async def test_setup_with_adbkey(hass):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
@ -317,8 +324,10 @@ async def test_sources(hass, config0):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
@ -395,8 +404,10 @@ async def _test_exclude_sources(hass, config0, expected_sources):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
@ -475,8 +486,10 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch)
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
@ -701,8 +714,10 @@ async def test_setup_fail(hass, config):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id) is False
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is None
|
||||
@ -718,8 +733,9 @@ async def test_adb_command(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
|
||||
@ -747,8 +763,9 @@ async def test_adb_command_unicode_decode_error(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell",
|
||||
@ -776,8 +793,9 @@ async def test_adb_command_key(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.adb_shell", return_value=response
|
||||
@ -805,8 +823,9 @@ async def test_adb_command_get_properties(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.get_properties_dict",
|
||||
@ -834,8 +853,9 @@ async def test_learn_sendevent(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.learn_sendevent",
|
||||
@ -862,8 +882,9 @@ async def test_update_lock_not_acquired(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
@ -897,8 +918,9 @@ async def test_download(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed download because path is not whitelisted
|
||||
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_pull") as patch_pull:
|
||||
@ -943,8 +965,9 @@ async def test_upload(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Failed upload because path is not whitelisted
|
||||
with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_push") as patch_push:
|
||||
@ -987,8 +1010,9 @@ async def test_androidtv_volume_set(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.basetv.basetv_async.BaseTVAsync.set_volume_level", return_value=0.5
|
||||
@ -1014,8 +1038,9 @@ async def test_get_image(hass, hass_ws_client):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell("11")[patch_key]:
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
@ -1090,8 +1115,9 @@ async def test_services_androidtv(hass):
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
await _test_service(
|
||||
@ -1136,8 +1162,9 @@ async def test_services_firetv(hass):
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
await _test_service(hass, entity_id, SERVICE_MEDIA_STOP, "back")
|
||||
@ -1152,8 +1179,9 @@ async def test_volume_mute(hass):
|
||||
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]:
|
||||
with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patchers.patch_shell(SHELL_RESPONSE_STANDBY)[patch_key]:
|
||||
service_data = {ATTR_ENTITY_ID: entity_id, ATTR_MEDIA_VOLUME_MUTED: True}
|
||||
@ -1196,8 +1224,9 @@ async def test_connection_closed_on_ha_stop(hass):
|
||||
with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[
|
||||
patch_key
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close"
|
||||
@ -1220,8 +1249,9 @@ async def test_exception(hass):
|
||||
], patchers.patch_shell(SHELL_RESPONSE_OFF)[
|
||||
patch_key
|
||||
], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
with patchers.PATCH_DEVICE_PROPERTIES:
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.helpers.entity_component.async_update_entity(entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
|
@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from aiogithubapi import GitHubException
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.github.config_flow import starred_repositories
|
||||
from homeassistant.components.github.config_flow import get_repositories
|
||||
from homeassistant.components.github.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_REPOSITORIES,
|
||||
@ -161,11 +161,19 @@ async def test_starred_pagination_with_paginated_result(hass: HomeAssistant) ->
|
||||
last_page_number=2,
|
||||
data=[MagicMock(full_name="home-assistant/core")],
|
||||
)
|
||||
)
|
||||
),
|
||||
repos=AsyncMock(
|
||||
return_value=MagicMock(
|
||||
is_last_page=False,
|
||||
next_page_number=2,
|
||||
last_page_number=2,
|
||||
data=[MagicMock(full_name="awesome/reposiotry")],
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
):
|
||||
repos = await starred_repositories(hass, MOCK_ACCESS_TOKEN)
|
||||
repos = await get_repositories(hass, MOCK_ACCESS_TOKEN)
|
||||
|
||||
assert len(repos) == 2
|
||||
assert repos[-1] == DEFAULT_REPOSITORIES[0]
|
||||
@ -182,11 +190,17 @@ async def test_starred_pagination_with_no_starred(hass: HomeAssistant) -> None:
|
||||
is_last_page=True,
|
||||
data=[],
|
||||
)
|
||||
)
|
||||
),
|
||||
repos=AsyncMock(
|
||||
return_value=MagicMock(
|
||||
is_last_page=True,
|
||||
data=[],
|
||||
)
|
||||
),
|
||||
)
|
||||
),
|
||||
):
|
||||
repos = await starred_repositories(hass, MOCK_ACCESS_TOKEN)
|
||||
repos = await get_repositories(hass, MOCK_ACCESS_TOKEN)
|
||||
|
||||
assert len(repos) == 2
|
||||
assert repos == DEFAULT_REPOSITORIES
|
||||
@ -200,7 +214,7 @@ async def test_starred_pagination_with_exception(hass: HomeAssistant) -> None:
|
||||
user=MagicMock(starred=AsyncMock(side_effect=GitHubException("Error")))
|
||||
),
|
||||
):
|
||||
repos = await starred_repositories(hass, MOCK_ACCESS_TOKEN)
|
||||
repos = await get_repositories(hass, MOCK_ACCESS_TOKEN)
|
||||
|
||||
assert len(repos) == 2
|
||||
assert repos == DEFAULT_REPOSITORIES
|
||||
|
@ -12,6 +12,8 @@ from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_
|
||||
|
||||
from .generator import get_mock_device
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -88,6 +90,37 @@ async def test_discovery_flow_works(hass, aioclient_mock):
|
||||
assert result["result"].unique_id == "HWE-P1_aabbccddeeff"
|
||||
|
||||
|
||||
async def test_config_flow_imports_entry(aioclient_mock, hass):
|
||||
"""Test config flow accepts imported configuration."""
|
||||
|
||||
device = get_mock_device()
|
||||
|
||||
mock_entry = MockConfigEntry(domain="homewizard_energy", data={"host": "1.2.3.4"})
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
with patch("aiohwenergy.HomeWizardEnergy", return_value=device,), patch(
|
||||
"homeassistant.components.homewizard.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_IMPORT,
|
||||
"old_config_entry_id": mock_entry.entry_id,
|
||||
},
|
||||
data=mock_entry.data,
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == f"{device.device.product_name} (aabbccddeeff)"
|
||||
assert result["data"][CONF_IP_ADDRESS] == "1.2.3.4"
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert len(device.initialize.mock_calls) == 1
|
||||
assert len(device.close.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_discovery_disabled_api(hass, aioclient_mock):
|
||||
"""Test discovery detecting disabled api."""
|
||||
|
||||
|
@ -4,9 +4,11 @@ from unittest.mock import patch
|
||||
|
||||
from aiohwenergy import AiohwenergyException, DisabledError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.homewizard.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_IP_ADDRESS
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .generator import get_mock_device
|
||||
|
||||
@ -68,6 +70,94 @@ async def test_load_failed_host_unavailable(aioclient_mock, hass):
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass):
|
||||
"""Test config flow accepts imported configuration."""
|
||||
|
||||
device = get_mock_device()
|
||||
|
||||
# Add original entry
|
||||
original_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_IP_ADDRESS: "1.2.3.4"},
|
||||
entry_id="old_id",
|
||||
)
|
||||
original_entry.add_to_hass(hass)
|
||||
|
||||
# Give it some entities to see of they migrate properly
|
||||
ent_reg = er.async_get(hass)
|
||||
old_entity_active_power = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
"homewizard_energy",
|
||||
"p1_active_power_unique_id",
|
||||
config_entry=original_entry,
|
||||
original_name="Active Power",
|
||||
suggested_object_id="p1_active_power",
|
||||
)
|
||||
old_entity_switch = ent_reg.async_get_or_create(
|
||||
"switch",
|
||||
"homewizard_energy",
|
||||
"socket_switch_unique_id",
|
||||
config_entry=original_entry,
|
||||
original_name="Switch",
|
||||
suggested_object_id="socket_switch",
|
||||
)
|
||||
old_entity_disabled_sensor = ent_reg.async_get_or_create(
|
||||
"sensor",
|
||||
"homewizard_energy",
|
||||
"socket_disabled_unique_id",
|
||||
config_entry=original_entry,
|
||||
original_name="Switch Disabled",
|
||||
suggested_object_id="socket_disabled",
|
||||
disabled_by=er.DISABLED_USER,
|
||||
)
|
||||
# Update some user-customs
|
||||
ent_reg.async_update_entity(old_entity_active_power.entity_id, name="new_name")
|
||||
ent_reg.async_update_entity(old_entity_switch.entity_id, icon="new_icon")
|
||||
|
||||
imported_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_IP_ADDRESS: "1.2.3.4", "old_config_entry_id": "old_id"},
|
||||
source=config_entries.SOURCE_IMPORT,
|
||||
entry_id="new_id",
|
||||
)
|
||||
imported_entry.add_to_hass(hass)
|
||||
|
||||
# Add the entry_id to trigger migration
|
||||
with patch(
|
||||
"aiohwenergy.HomeWizardEnergy",
|
||||
return_value=device,
|
||||
):
|
||||
await hass.config_entries.async_setup(imported_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert original_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert imported_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Check if new entities are migrated
|
||||
new_entity_active_power = ent_reg.async_get(old_entity_active_power.entity_id)
|
||||
assert new_entity_active_power.platform == DOMAIN
|
||||
assert new_entity_active_power.name == "new_name"
|
||||
assert new_entity_active_power.icon is None
|
||||
assert new_entity_active_power.original_name == "Active Power"
|
||||
assert new_entity_active_power.unique_id == "p1_active_power_unique_id"
|
||||
assert new_entity_active_power.disabled_by is None
|
||||
|
||||
new_entity_switch = ent_reg.async_get(old_entity_switch.entity_id)
|
||||
assert new_entity_switch.platform == DOMAIN
|
||||
assert new_entity_switch.name is None
|
||||
assert new_entity_switch.icon == "new_icon"
|
||||
assert new_entity_switch.original_name == "Switch"
|
||||
assert new_entity_switch.unique_id == "socket_switch_unique_id"
|
||||
assert new_entity_switch.disabled_by is None
|
||||
|
||||
new_entity_disabled_sensor = ent_reg.async_get(old_entity_disabled_sensor.entity_id)
|
||||
assert new_entity_disabled_sensor.platform == DOMAIN
|
||||
assert new_entity_disabled_sensor.name is None
|
||||
assert new_entity_disabled_sensor.original_name == "Switch Disabled"
|
||||
assert new_entity_disabled_sensor.unique_id == "socket_disabled_unique_id"
|
||||
assert new_entity_disabled_sensor.disabled_by == er.DISABLED_USER
|
||||
|
||||
|
||||
async def test_load_detect_api_disabled(aioclient_mock, hass):
|
||||
"""Test setup detects disabled API."""
|
||||
|
||||
|
@ -1899,7 +1899,8 @@ async def test_light_service_call_color_temp_conversion(
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "color_temp": 153}
|
||||
_, data = entity1.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 0, 255)}
|
||||
# Home Assistant uses RGBCW so a mireds of 153 should be maximum cold at 100% brightness so 255
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 255, 0)}
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
@ -1917,7 +1918,63 @@ async def test_light_service_call_color_temp_conversion(
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "color_temp": 500}
|
||||
_, data = entity1.last_call("turn_on")
|
||||
assert data == {"brightness": 128, "rgbww_color": (0, 0, 0, 128, 0)}
|
||||
# Home Assistant uses RGBCW so a mireds of 500 should be maximum warm at 50% brightness so 128
|
||||
assert data == {"brightness": 128, "rgbww_color": (0, 0, 0, 0, 128)}
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": [
|
||||
entity0.entity_id,
|
||||
entity1.entity_id,
|
||||
],
|
||||
"brightness_pct": 100,
|
||||
"color_temp": 327,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "color_temp": 327}
|
||||
_, data = entity1.last_call("turn_on")
|
||||
# Home Assistant uses RGBCW so a mireds of 328 should be the midway point at 100% brightness so 127 (rounding), 128
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 127, 128)}
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": [
|
||||
entity0.entity_id,
|
||||
entity1.entity_id,
|
||||
],
|
||||
"brightness_pct": 100,
|
||||
"color_temp": 240,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "color_temp": 240}
|
||||
_, data = entity1.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 191, 64)}
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": [
|
||||
entity0.entity_id,
|
||||
entity1.entity_id,
|
||||
],
|
||||
"brightness_pct": 100,
|
||||
"color_temp": 410,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "color_temp": 410}
|
||||
_, data = entity1.last_call("turn_on")
|
||||
assert data == {"brightness": 255, "rgbww_color": (0, 0, 0, 66, 189)}
|
||||
|
||||
|
||||
async def test_light_service_call_white_mode(hass, enable_custom_integrations):
|
||||
|
@ -5,7 +5,7 @@ import importlib
|
||||
import sqlite3
|
||||
import sys
|
||||
import threading
|
||||
from unittest.mock import ANY, Mock, PropertyMock, call, patch
|
||||
from unittest.mock import Mock, PropertyMock, call, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, text
|
||||
@ -57,7 +57,7 @@ async def test_schema_update_calls(hass):
|
||||
assert recorder.util.async_migration_in_progress(hass) is False
|
||||
update.assert_has_calls(
|
||||
[
|
||||
call(hass.data[DATA_INSTANCE], ANY, version + 1, 0)
|
||||
call(hass.data[DATA_INSTANCE], version + 1, 0)
|
||||
for version in range(0, models.SCHEMA_VERSION)
|
||||
]
|
||||
)
|
||||
@ -309,7 +309,7 @@ async def test_schema_migrate(hass, start_version):
|
||||
def test_invalid_update():
|
||||
"""Test that an invalid new version raises an exception."""
|
||||
with pytest.raises(ValueError):
|
||||
migration._apply_update(Mock(), Mock(), -1, 0)
|
||||
migration._apply_update(Mock(), -1, 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -324,9 +324,13 @@ def test_invalid_update():
|
||||
def test_modify_column(engine_type, substr):
|
||||
"""Test that modify column generates the expected query."""
|
||||
connection = Mock()
|
||||
session = Mock()
|
||||
session.connection = Mock(return_value=connection)
|
||||
instance = Mock()
|
||||
instance.get_session = Mock(return_value=session)
|
||||
engine = Mock()
|
||||
engine.dialect.name = engine_type
|
||||
migration._modify_columns(connection, engine, "events", ["event_type VARCHAR(64)"])
|
||||
migration._modify_columns(instance, engine, "events", ["event_type VARCHAR(64)"])
|
||||
if substr:
|
||||
assert substr in connection.execute.call_args[0][0].text
|
||||
else:
|
||||
@ -338,8 +342,10 @@ def test_forgiving_add_column():
|
||||
engine = create_engine("sqlite://", poolclass=StaticPool)
|
||||
with Session(engine) as session:
|
||||
session.execute(text("CREATE TABLE hello (id int)"))
|
||||
migration._add_columns(session, "hello", ["context_id CHARACTER(36)"])
|
||||
migration._add_columns(session, "hello", ["context_id CHARACTER(36)"])
|
||||
instance = Mock()
|
||||
instance.get_session = Mock(return_value=session)
|
||||
migration._add_columns(instance, "hello", ["context_id CHARACTER(36)"])
|
||||
migration._add_columns(instance, "hello", ["context_id CHARACTER(36)"])
|
||||
|
||||
|
||||
def test_forgiving_add_index():
|
||||
@ -347,7 +353,9 @@ def test_forgiving_add_index():
|
||||
engine = create_engine("sqlite://", poolclass=StaticPool)
|
||||
models.Base.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
migration._create_index(session, "states", "ix_states_context_id")
|
||||
instance = Mock()
|
||||
instance.get_session = Mock(return_value=session)
|
||||
migration._create_index(instance, "states", "ix_states_context_id")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -852,7 +852,6 @@ def test_delete_duplicates(caplog, tmpdir):
|
||||
|
||||
assert "Deleted 2 duplicated statistics rows" in caplog.text
|
||||
assert "Found non identical" not in caplog.text
|
||||
assert "Found more than" not in caplog.text
|
||||
assert "Found duplicated" not in caplog.text
|
||||
|
||||
|
||||
@ -989,7 +988,6 @@ def test_delete_duplicates_non_identical(caplog, tmpdir):
|
||||
|
||||
assert "Deleted 2 duplicated statistics rows" in caplog.text
|
||||
assert "Deleted 1 non identical" in caplog.text
|
||||
assert "Found more than" not in caplog.text
|
||||
assert "Found duplicated" not in caplog.text
|
||||
|
||||
isotime = dt_util.utcnow().isoformat()
|
||||
@ -1028,144 +1026,6 @@ def test_delete_duplicates_non_identical(caplog, tmpdir):
|
||||
]
|
||||
|
||||
|
||||
@patch.object(statistics, "MAX_DUPLICATES", 2)
|
||||
def test_delete_duplicates_too_many(caplog, tmpdir):
|
||||
"""Test removal of duplicated statistics."""
|
||||
test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db")
|
||||
dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}"
|
||||
|
||||
module = "tests.components.recorder.models_schema_23"
|
||||
importlib.import_module(module)
|
||||
old_models = sys.modules[module]
|
||||
|
||||
period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00"))
|
||||
period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00"))
|
||||
period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00"))
|
||||
period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00"))
|
||||
|
||||
external_energy_statistics_1 = (
|
||||
{
|
||||
"start": period1,
|
||||
"last_reset": None,
|
||||
"state": 0,
|
||||
"sum": 2,
|
||||
},
|
||||
{
|
||||
"start": period2,
|
||||
"last_reset": None,
|
||||
"state": 1,
|
||||
"sum": 3,
|
||||
},
|
||||
{
|
||||
"start": period3,
|
||||
"last_reset": None,
|
||||
"state": 2,
|
||||
"sum": 4,
|
||||
},
|
||||
{
|
||||
"start": period4,
|
||||
"last_reset": None,
|
||||
"state": 3,
|
||||
"sum": 5,
|
||||
},
|
||||
{
|
||||
"start": period4,
|
||||
"last_reset": None,
|
||||
"state": 3,
|
||||
"sum": 5,
|
||||
},
|
||||
)
|
||||
external_energy_metadata_1 = {
|
||||
"has_mean": False,
|
||||
"has_sum": True,
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import_tariff_1",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
external_energy_statistics_2 = (
|
||||
{
|
||||
"start": period1,
|
||||
"last_reset": None,
|
||||
"state": 0,
|
||||
"sum": 20,
|
||||
},
|
||||
{
|
||||
"start": period2,
|
||||
"last_reset": None,
|
||||
"state": 1,
|
||||
"sum": 30,
|
||||
},
|
||||
{
|
||||
"start": period3,
|
||||
"last_reset": None,
|
||||
"state": 2,
|
||||
"sum": 40,
|
||||
},
|
||||
{
|
||||
"start": period4,
|
||||
"last_reset": None,
|
||||
"state": 3,
|
||||
"sum": 50,
|
||||
},
|
||||
{
|
||||
"start": period4,
|
||||
"last_reset": None,
|
||||
"state": 3,
|
||||
"sum": 50,
|
||||
},
|
||||
)
|
||||
external_energy_metadata_2 = {
|
||||
"has_mean": False,
|
||||
"has_sum": True,
|
||||
"name": "Total imported energy",
|
||||
"source": "test",
|
||||
"statistic_id": "test:total_energy_import_tariff_2",
|
||||
"unit_of_measurement": "kWh",
|
||||
}
|
||||
|
||||
# Create some duplicated statistics with schema version 23
|
||||
with patch.object(recorder, "models", old_models), patch.object(
|
||||
recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION
|
||||
), patch(
|
||||
"homeassistant.components.recorder.create_engine", new=_create_engine_test
|
||||
):
|
||||
hass = get_test_home_assistant()
|
||||
setup_component(hass, "recorder", {"recorder": {"db_url": dburl}})
|
||||
wait_recording_done(hass)
|
||||
wait_recording_done(hass)
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
session.add(
|
||||
recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1)
|
||||
)
|
||||
session.add(
|
||||
recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2)
|
||||
)
|
||||
with session_scope(hass=hass) as session:
|
||||
for stat in external_energy_statistics_1:
|
||||
session.add(recorder.models.Statistics.from_stats(1, stat))
|
||||
for stat in external_energy_statistics_2:
|
||||
session.add(recorder.models.Statistics.from_stats(2, stat))
|
||||
|
||||
hass.stop()
|
||||
|
||||
# Test that the duplicates are removed during migration from schema 23
|
||||
hass = get_test_home_assistant()
|
||||
hass.config.config_dir = tmpdir
|
||||
setup_component(hass, "recorder", {"recorder": {"db_url": dburl}})
|
||||
hass.start()
|
||||
wait_recording_done(hass)
|
||||
wait_recording_done(hass)
|
||||
hass.stop()
|
||||
|
||||
assert "Deleted 2 duplicated statistics rows" in caplog.text
|
||||
assert "Found non identical" not in caplog.text
|
||||
assert "Found more than 1 duplicated statistic rows" in caplog.text
|
||||
assert "Found duplicated" not in caplog.text
|
||||
|
||||
|
||||
@patch.object(statistics, "MAX_DUPLICATES", 2)
|
||||
def test_delete_duplicates_short_term(caplog, tmpdir):
|
||||
"""Test removal of duplicated statistics."""
|
||||
test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db")
|
||||
@ -1228,7 +1088,6 @@ def test_delete_duplicates_short_term(caplog, tmpdir):
|
||||
|
||||
assert "duplicated statistics rows" not in caplog.text
|
||||
assert "Found non identical" not in caplog.text
|
||||
assert "Found more than" not in caplog.text
|
||||
assert "Deleted duplicated short term statistic" in caplog.text
|
||||
|
||||
|
||||
@ -1240,7 +1099,6 @@ def test_delete_duplicates_no_duplicates(hass_recorder, caplog):
|
||||
delete_duplicates(hass.data[DATA_INSTANCE], session)
|
||||
assert "duplicated statistics rows" not in caplog.text
|
||||
assert "Found non identical" not in caplog.text
|
||||
assert "Found more than" not in caplog.text
|
||||
assert "Found duplicated" not in caplog.text
|
||||
|
||||
|
||||
|
@ -39,7 +39,7 @@ def test_get_device_detects_dimmer(mock_openzwave):
|
||||
device = light.get_device(node=node, values=values, node_config={})
|
||||
assert isinstance(device, light.ZwaveDimmer)
|
||||
assert device.color_mode == COLOR_MODE_BRIGHTNESS
|
||||
assert device.supported_features is None
|
||||
assert device.supported_features == 0
|
||||
assert device.supported_color_modes == {COLOR_MODE_BRIGHTNESS}
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ def test_get_device_detects_colorlight(mock_openzwave):
|
||||
device = light.get_device(node=node, values=values, node_config={})
|
||||
assert isinstance(device, light.ZwaveColorLight)
|
||||
assert device.color_mode == COLOR_MODE_RGB
|
||||
assert device.supported_features is None
|
||||
assert device.supported_features == 0
|
||||
assert device.supported_color_modes == {COLOR_MODE_RGB}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ def test_get_device_detects_zw098(mock_openzwave):
|
||||
device = light.get_device(node=node, values=values, node_config={})
|
||||
assert isinstance(device, light.ZwaveColorLight)
|
||||
assert device.color_mode == COLOR_MODE_RGB
|
||||
assert device.supported_features is None
|
||||
assert device.supported_features == 0
|
||||
assert device.supported_color_modes == {COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB}
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ def test_get_device_detects_rgbw_light(mock_openzwave):
|
||||
device.value_added()
|
||||
assert isinstance(device, light.ZwaveColorLight)
|
||||
assert device.color_mode == COLOR_MODE_RGBW
|
||||
assert device.supported_features is None
|
||||
assert device.supported_features == 0
|
||||
assert device.supported_color_modes == {COLOR_MODE_RGBW}
|
||||
|
||||
|
||||
|
@ -596,6 +596,23 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration):
|
||||
== INVALID_CONFIG
|
||||
)
|
||||
|
||||
# Test invalid device ID fails validation
|
||||
with pytest.raises(InvalidDeviceAutomationConfig):
|
||||
await device_condition.async_validate_condition_config(
|
||||
hass,
|
||||
{
|
||||
"condition": "device",
|
||||
"domain": DOMAIN,
|
||||
"type": "value",
|
||||
"device_id": "invalid_device_id",
|
||||
"command_class": CommandClass.DOOR_LOCK.value,
|
||||
"property": 9999,
|
||||
"property_key": 9999,
|
||||
"endpoint": 9999,
|
||||
"value": 9999,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_get_value_from_config_failure(
|
||||
hass, client, hank_binary_switch, integration
|
||||
|
@ -1370,3 +1370,19 @@ async def test_failure_scenarios(hass, client, hank_binary_switch, integration):
|
||||
await device_trigger.async_validate_trigger_config(hass, INVALID_CONFIG)
|
||||
== INVALID_CONFIG
|
||||
)
|
||||
|
||||
# Test invalid device ID fails validation
|
||||
with pytest.raises(InvalidDeviceAutomationConfig):
|
||||
await device_trigger.async_validate_trigger_config(
|
||||
hass,
|
||||
{
|
||||
"platform": "device",
|
||||
"domain": DOMAIN,
|
||||
"device_id": "invalid_device_id",
|
||||
"type": "zwave_js.value_updated.value",
|
||||
"command_class": CommandClass.DOOR_LOCK.value,
|
||||
"property": 9999,
|
||||
"property_key": 9999,
|
||||
"endpoint": 9999,
|
||||
},
|
||||
)
|
||||
|
@ -18,15 +18,15 @@ async def test_container_installationtype(hass):
|
||||
"""Test container installation type."""
|
||||
with patch("platform.system", return_value="Linux"), patch(
|
||||
"os.path.isfile", return_value=True
|
||||
):
|
||||
), patch("homeassistant.helpers.system_info.getuser", return_value="root"):
|
||||
info = await hass.helpers.system_info.async_get_system_info()
|
||||
assert info["installation_type"] == "Home Assistant Container"
|
||||
|
||||
with patch("platform.system", return_value="Linux"), patch(
|
||||
"os.path.isfile", return_value=True
|
||||
"os.path.isfile", side_effect=lambda file: file == "/.dockerenv"
|
||||
), patch("homeassistant.helpers.system_info.getuser", return_value="user"):
|
||||
info = await hass.helpers.system_info.async_get_system_info()
|
||||
assert info["installation_type"] == "Unknown"
|
||||
assert info["installation_type"] == "Unsupported Third Party Container"
|
||||
|
||||
|
||||
async def test_getuser_keyerror(hass):
|
||||
|
@ -406,46 +406,137 @@ def test_color_rgb_to_rgbww():
|
||||
|
||||
|
||||
def test_color_temperature_to_rgbww():
|
||||
"""Test color temp to warm, cold conversion."""
|
||||
"""Test color temp to warm, cold conversion.
|
||||
|
||||
Temperature values must be in mireds
|
||||
Home Assistant uses rgbcw for rgbww
|
||||
"""
|
||||
assert color_util.color_temperature_to_rgbww(153, 255, 153, 500) == (
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
0,
|
||||
)
|
||||
assert color_util.color_temperature_to_rgbww(153, 128, 153, 500) == (
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
128,
|
||||
0,
|
||||
)
|
||||
assert color_util.color_temperature_to_rgbww(500, 255, 153, 500) == (
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
0,
|
||||
255,
|
||||
)
|
||||
assert color_util.color_temperature_to_rgbww(500, 128, 153, 500) == (
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
128,
|
||||
0,
|
||||
128,
|
||||
)
|
||||
assert color_util.color_temperature_to_rgbww(347, 255, 153, 500) == (
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
143,
|
||||
112,
|
||||
143,
|
||||
)
|
||||
assert color_util.color_temperature_to_rgbww(347, 128, 153, 500) == (
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
72,
|
||||
56,
|
||||
72,
|
||||
)
|
||||
|
||||
|
||||
def test_rgbww_to_color_temperature():
|
||||
"""Test rgbww conversion to color temp.
|
||||
|
||||
Temperature values must be in mireds
|
||||
Home Assistant uses rgbcw for rgbww
|
||||
"""
|
||||
assert (
|
||||
color_util.rgbww_to_color_temperature(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
0,
|
||||
),
|
||||
153,
|
||||
500,
|
||||
)
|
||||
== (153, 255)
|
||||
)
|
||||
assert color_util.rgbww_to_color_temperature((0, 0, 0, 128, 0), 153, 500) == (
|
||||
153,
|
||||
128,
|
||||
)
|
||||
assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 255), 153, 500) == (
|
||||
500,
|
||||
255,
|
||||
)
|
||||
assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 128), 153, 500) == (
|
||||
500,
|
||||
128,
|
||||
)
|
||||
assert color_util.rgbww_to_color_temperature((0, 0, 0, 112, 143), 153, 500) == (
|
||||
348,
|
||||
255,
|
||||
)
|
||||
assert color_util.rgbww_to_color_temperature((0, 0, 0, 56, 72), 153, 500) == (
|
||||
348,
|
||||
128,
|
||||
)
|
||||
assert color_util.rgbww_to_color_temperature((0, 0, 0, 0, 0), 153, 500) == (
|
||||
500,
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def test_white_levels_to_color_temperature():
|
||||
"""Test warm, cold conversion to color temp.
|
||||
|
||||
Temperature values must be in mireds
|
||||
Home Assistant uses rgbcw for rgbww
|
||||
"""
|
||||
assert (
|
||||
color_util.while_levels_to_color_temperature(
|
||||
255,
|
||||
0,
|
||||
153,
|
||||
500,
|
||||
)
|
||||
== (153, 255)
|
||||
)
|
||||
assert color_util.while_levels_to_color_temperature(128, 0, 153, 500) == (
|
||||
153,
|
||||
128,
|
||||
)
|
||||
assert color_util.while_levels_to_color_temperature(0, 255, 153, 500) == (
|
||||
500,
|
||||
255,
|
||||
)
|
||||
assert color_util.while_levels_to_color_temperature(0, 128, 153, 500) == (
|
||||
500,
|
||||
128,
|
||||
)
|
||||
assert color_util.while_levels_to_color_temperature(112, 143, 153, 500) == (
|
||||
348,
|
||||
255,
|
||||
)
|
||||
assert color_util.while_levels_to_color_temperature(56, 72, 153, 500) == (
|
||||
348,
|
||||
128,
|
||||
)
|
||||
assert color_util.while_levels_to_color_temperature(0, 0, 153, 500) == (
|
||||
500,
|
||||
0,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user