Skip to content
Merged
11 changes: 6 additions & 5 deletions homeassistant/components/recorder/auto_repairs/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sqlalchemy.orm.attributes import InstrumentedAttribute

from ..const import SupportedDialect
from ..db_schema import DOUBLE_PRECISION_TYPE_SQL, DOUBLE_TYPE
from ..db_schema import DOUBLE_PRECISION_TYPE_SQL, DOUBLE_TYPE, MYSQL_COLLATE
from ..util import session_scope

if TYPE_CHECKING:
Expand Down Expand Up @@ -105,12 +105,13 @@ def _validate_table_schema_has_correct_collation(
or dialect_kwargs.get("mariadb_collate")
or connection.dialect._fetch_setting(connection, "collation_server") # type: ignore[attr-defined] # noqa: SLF001
)
if collate and collate != "utf8mb4_unicode_ci":
if collate and collate != MYSQL_COLLATE:
_LOGGER.debug(
"Database %s collation is not utf8mb4_unicode_ci",
"Database %s collation is not %s",
table,
MYSQL_COLLATE,
)
schema_errors.add(f"{table}.utf8mb4_unicode_ci")
schema_errors.add(f"{table}.{MYSQL_COLLATE}")
return schema_errors


Expand Down Expand Up @@ -240,7 +241,7 @@ def correct_db_schema_utf8(
table_name = table_object.__tablename__
if (
f"{table_name}.4-byte UTF-8" in schema_errors
or f"{table_name}.utf8mb4_unicode_ci" in schema_errors
or f"{table_name}.{MYSQL_COLLATE}" in schema_errors
):
from ..migration import ( # noqa: PLC0415
_correct_table_character_set_and_collation,
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/recorder/db_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class LegacyBase(DeclarativeBase):
"""Base class for tables, used for schema migration."""


SCHEMA_VERSION = 52
SCHEMA_VERSION = 53

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -128,7 +128,7 @@ class LegacyBase(DeclarativeBase):
LEGACY_MAX_LENGTH_EVENT_CONTEXT_ID: Final = 36
CONTEXT_ID_BIN_MAX_LENGTH = 16

MYSQL_COLLATE = "utf8mb4_unicode_ci"
MYSQL_COLLATE = "utf8mb4_bin"
MYSQL_DEFAULT_CHARSET = "utf8mb4"
MYSQL_ENGINE = "InnoDB"

Expand Down
23 changes: 21 additions & 2 deletions homeassistant/components/recorder/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ def _apply_update(self) -> None:
class _SchemaVersion21Migrator(_SchemaVersionMigrator, target_version=21):
def _apply_update(self) -> None:
"""Version specific update method."""
# Try to change the character set of the statistic_meta table
# Try to change the character set of events, states and statistics_meta tables
if self.engine.dialect.name == SupportedDialect.MYSQL:
for table in ("events", "states", "statistics_meta"):
_correct_table_character_set_and_collation(table, self.session_maker)
Expand Down Expand Up @@ -2125,6 +2125,23 @@ def _apply_update_postgresql_sqlite(self) -> None:
)


class _SchemaVersion53Migrator(_SchemaVersionMigrator, target_version=53):
def _apply_update(self) -> None:
"""Version specific update method."""
# Try to change the character set of events, states and statistics_meta tables
if self.engine.dialect.name == SupportedDialect.MYSQL:
for table in (
"events",
"event_data",
"states",
"state_attributes",
"statistics",
"statistics_meta",
"statistics_short_term",
):
_correct_table_character_set_and_collation(table, self.session_maker)


def _migrate_statistics_columns_to_timestamp_removing_duplicates(
hass: HomeAssistant,
instance: Recorder,
Expand Down Expand Up @@ -2167,8 +2184,10 @@ def _correct_table_character_set_and_collation(
"""Correct issues detected by validate_db_schema."""
# Attempt to convert the table to utf8mb4
_LOGGER.warning(
"Updating character set and collation of table %s to utf8mb4. %s",
"Updating table %s to character set %s and collation %s. %s",
table,
MYSQL_DEFAULT_CHARSET,
MYSQL_COLLATE,
MIGRATION_NOTE_MINUTES,
)
with (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_wall_connector",
"iot_class": "local_polling",
"loggers": ["tesla_wall_connector"],
"requirements": ["tesla-wall-connector==1.0.2"]
"requirements": ["tesla-wall-connector==1.1.0"]
}
18 changes: 3 additions & 15 deletions homeassistant/components/tuya/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

from __future__ import annotations

from contextlib import suppress
import json
from typing import Any, cast
from typing import Any

from tuya_sharing import CustomerDevice

Expand Down Expand Up @@ -101,30 +99,20 @@ def _async_device_as_dict(
data["status"][dpcode] = REDACTED
continue

with suppress(ValueError, TypeError):
value = json.loads(value)
data["status"][dpcode] = value

# Gather Tuya functions
for function in device.function.values():
value = function.values
with suppress(ValueError, TypeError, AttributeError):
value = json.loads(cast(str, function.values))

data["function"][function.code] = {
"type": function.type,
"value": value,
"value": function.values,
}

# Gather Tuya status ranges
for status_range in device.status_range.values():
value = status_range.values
with suppress(ValueError, TypeError, AttributeError):
value = json.loads(status_range.values)

data["status_range"][status_range.code] = {
"type": status_range.type,
"value": value,
"value": status_range.values,
}

# Gather information how this Tuya device is represented in Home Assistant
Expand Down
17 changes: 9 additions & 8 deletions homeassistant/components/tuya/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from homeassistant.util.json import json_loads_object

from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType, WorkMode
Expand Down Expand Up @@ -499,11 +500,11 @@ def __init__(
values = self.device.status_range[dpcode].values

# Fetch color data type information
if function_data := json.loads(values):
if function_data := json_loads_object(values):
self._color_data_type = ColorTypeData(
h_type=IntegerTypeData(dpcode, **function_data["h"]),
s_type=IntegerTypeData(dpcode, **function_data["s"]),
v_type=IntegerTypeData(dpcode, **function_data["v"]),
h_type=IntegerTypeData(dpcode, **cast(dict, function_data["h"])),
s_type=IntegerTypeData(dpcode, **cast(dict, function_data["s"])),
v_type=IntegerTypeData(dpcode, **cast(dict, function_data["v"])),
)
else:
# If no type is found, use a default one
Expand Down Expand Up @@ -770,12 +771,12 @@ def _get_color_data(self) -> ColorData | None:
if not (status_data := self.device.status[self._color_data_dpcode]):
return None

if not (status := json.loads(status_data)):
if not (status := json_loads_object(status_data)):
return None

return ColorData(
type_data=self._color_data_type,
h_value=status["h"],
s_value=status["s"],
v_value=status["v"],
h_value=cast(int, status["h"]),
s_value=cast(int, status["s"]),
v_value=cast(int, status["v"]),
)
15 changes: 7 additions & 8 deletions homeassistant/components/tuya/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
from abc import ABC, abstractmethod
import base64
from dataclasses import dataclass
import json
from typing import Any, Literal, Self, overload
from typing import Any, Literal, Self, cast, overload

from tuya_sharing import CustomerDevice

from homeassistant.util.json import json_loads
from homeassistant.util.json import json_loads, json_loads_object

from .const import DPCode, DPType
from .util import parse_dptype, remap_value
Expand Down Expand Up @@ -88,7 +87,7 @@ def remap_value_from(
@classmethod
def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
"""Load JSON string and return a IntegerTypeData object."""
if not (parsed := json.loads(data)):
if not (parsed := cast(dict[str, Any] | None, json_loads_object(data))):
return None

return cls(
Expand All @@ -111,9 +110,9 @@ class BitmapTypeInformation(TypeInformation):
@classmethod
def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
"""Load JSON string and return a BitmapTypeInformation object."""
if not (parsed := json.loads(data)):
if not (parsed := json_loads_object(data)):
return None
return cls(dpcode, **parsed)
return cls(dpcode, **cast(dict[str, list[str]], parsed))


@dataclass
Expand All @@ -125,9 +124,9 @@ class EnumTypeData(TypeInformation):
@classmethod
def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
"""Load JSON string and return a EnumTypeData object."""
if not (parsed := json.loads(data)):
if not (parsed := json_loads_object(data)):
return None
return cls(dpcode, **parsed)
return cls(dpcode, **cast(dict[str, list[str]], parsed))


_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion tests/components/lifx/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Tests for the lifx integration config flow."""

from collections.abc import Generator
from ipaddress import ip_address
import socket
from typing import Any
from unittest.mock import patch
from unittest.mock import AsyncMock, patch

import pytest

Expand Down Expand Up @@ -44,6 +45,15 @@
from tests.common import MockConfigEntry


@pytest.fixture(autouse=True)
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.lifx.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry


async def test_discovery(hass: HomeAssistant) -> None:
"""Test setting up discovery."""
with _patch_discovery(), _patch_config_flow_try_connect():
Expand Down Expand Up @@ -591,6 +601,7 @@ async def test_refuse_relays(hass: HomeAssistant) -> None:
assert result2["errors"] == {"base": "cannot_connect"}


@pytest.mark.parametrize("mock_setup_entry", [None]) # Disable the autouse fixture
async def test_suggested_area(
hass: HomeAssistant,
area_registry: ar.AreaRegistry,
Expand Down
14 changes: 8 additions & 6 deletions tests/components/lifx/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -1619,9 +1619,10 @@ async def test_transitions_brightness_only(hass: HomeAssistant) -> None:
bulb.get_color.reset_mock()

# Ensure we force an update after the transition
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
assert len(bulb.get_color.calls) == 2
with _patch_discovery(device=bulb):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
assert len(bulb.get_color.calls) == 2


async def test_transitions_color_bulb(hass: HomeAssistant) -> None:
Expand Down Expand Up @@ -1714,9 +1715,10 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None:
bulb.get_color.reset_mock()

# Ensure we force an update after the transition
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
assert len(bulb.get_color.calls) == 2
with _patch_discovery(device=bulb):
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))
await hass.async_block_till_done()
assert len(bulb.get_color.calls) == 2

bulb.set_power.reset_mock()
bulb.set_color.reset_mock()
Expand Down
Loading
Loading