Compare commits

...

24 Commits

Author SHA1 Message Date
Paulus Schoutsen
6397cc5d04 Bumped version to 2023.2.0b2 2023-01-26 21:47:21 -05:00
Jesse Hills
b7311dc655 Remove esphome password from config flow data if not needed (#86763)
* Remove esphome password if not needed

* Add test

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-01-26 21:47:00 -05:00
Paulus Schoutsen
e20c7491c1 ESPHome update: Store reference to runtime data, not one of its values (#86762)
Store reference to runtime data, not one of its values
2023-01-26 21:46:59 -05:00
Aaron Bach
8cbefd5f97 Fix state class issues in Ambient PWS (#86758)
fixes undefined
2023-01-26 21:46:58 -05:00
Paulus Schoutsen
c7665b479a OpenAI: Fix device without model (#86754) 2023-01-26 21:46:57 -05:00
Shay Levy
4f2966674a Bump aioshelly to 5.3.1 (#86751) 2023-01-26 21:46:56 -05:00
Franck Nijhof
b464179eac Fix state classes for duration device class (#86727) 2023-01-26 21:46:55 -05:00
Franck Nijhof
cd59705c4b Remove gas device class from current sensor in dsmr_reader (#86725) 2023-01-26 21:46:54 -05:00
Martin Hjelmare
77bd23899f Bump python-matter-server to 2.0.2 (#86712) 2023-01-26 21:46:53 -05:00
David F. Mulcahey
d211603ba7 Update Inovelli Blue Series switch support in ZHA (#86711) 2023-01-26 21:46:52 -05:00
Erik Montnemery
1dc3bb6eb1 Terminate strings at NUL when recording states and events (#86687) 2023-01-26 21:46:51 -05:00
Robert Svensson
22afc7c7fb Fix missing interface key in deCONZ logbook (#86684)
fixes undefined
2023-01-26 21:46:50 -05:00
Paulus Schoutsen
ba82f13821 Make openai conversation prompt template more readable + test case (#86676) 2023-01-26 21:46:49 -05:00
MHFDoge
41add96bab Add known webostv button to list (#86674)
Add known button to list.
2023-01-26 21:46:48 -05:00
Andrey Kupreychik
c8c3f4bef6 Update ndms2_client to 0.1.2 (#86624)
fix https://github.com/home-assistant/core/issues/86379
fixes undefined
2023-01-26 21:46:47 -05:00
Patrick ZAJDA
8cb8ecdae9 Migrate Nuki to new entity naming style (#80021)
Co-authored-by: Pascal Vizeli <pvizeli@syshack.ch>
2023-01-26 21:46:46 -05:00
Pascal Reeb
bd1371680f Add device registration to the Nuki component (#79806)
* Add device registration to the Nuki component

* Name is always given by the API

* implement pvizeli's suggestions

* switch device_registry to snake_case

* fix entity naming

* unify manufacturer names
2023-01-26 21:46:45 -05:00
Paulus Schoutsen
8f684e962a Bumped version to 2023.2.0b1 2023-01-25 23:00:35 -05:00
Paulus Schoutsen
07a1259db9 Add error handling for OpenAI (#86671)
* Add error handling for OpenAI

* Simplify area filtering

* better prompt
2023-01-25 23:00:28 -05:00
David F. Mulcahey
ea2bf34647 Bump ZHA quirks lib (#86669) 2023-01-25 23:00:27 -05:00
J. Nick Koston
a6fdf1d09a Correct units on mopeka battery voltage sensor (#86663) 2023-01-25 23:00:26 -05:00
Paulus Schoutsen
9ca04dbfa1 Google Assistant: unset agent on unload (#86635) 2023-01-25 23:00:24 -05:00
Paulus Schoutsen
e1c8dff536 Fix oauth2 error (#86634) 2023-01-25 23:00:23 -05:00
Joakim Plate
a1416b9044 Print expected device class units in error log (#86125) 2023-01-25 23:00:22 -05:00
36 changed files with 448 additions and 161 deletions

View File

@@ -128,7 +128,6 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_AQI_PM25_24H,
name="AQI PM2.5 24h avg",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=TYPE_AQI_PM25_IN,
@@ -140,7 +139,6 @@ SENSOR_DESCRIPTIONS = (
key=TYPE_AQI_PM25_IN_24H,
name="AQI PM2.5 indoor 24h avg",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=TYPE_BAROMABSIN,
@@ -182,7 +180,7 @@ SENSOR_DESCRIPTIONS = (
name="Event rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_FEELSLIKE,
@@ -287,7 +285,6 @@ SENSOR_DESCRIPTIONS = (
name="Last rain",
icon="mdi:water",
device_class=SensorDeviceClass.TIMESTAMP,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_PER_DAY,
@@ -315,7 +312,7 @@ SENSOR_DESCRIPTIONS = (
name="Monthly rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_PM25_24H,
@@ -586,7 +583,7 @@ SENSOR_DESCRIPTIONS = (
name="Lifetime rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=TYPE_UV,
@@ -599,7 +596,7 @@ SENSOR_DESCRIPTIONS = (
name="Weekly rain",
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
device_class=SensorDeviceClass.PRECIPITATION,
state_class=SensorStateClass.MEASUREMENT,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_WINDDIR,

View File

@@ -17,6 +17,10 @@ from .device_trigger import (
CONF_BUTTON_2,
CONF_BUTTON_3,
CONF_BUTTON_4,
CONF_BUTTON_5,
CONF_BUTTON_6,
CONF_BUTTON_7,
CONF_BUTTON_8,
CONF_CLOSE,
CONF_DIM_DOWN,
CONF_DIM_UP,
@@ -95,6 +99,10 @@ INTERFACES = {
CONF_BUTTON_2: "Button 2",
CONF_BUTTON_3: "Button 3",
CONF_BUTTON_4: "Button 4",
CONF_BUTTON_5: "Button 5",
CONF_BUTTON_6: "Button 6",
CONF_BUTTON_7: "Button 7",
CONF_BUTTON_8: "Button 8",
CONF_SIDE_1: "Side 1",
CONF_SIDE_2: "Side 2",
CONF_SIDE_3: "Side 3",

View File

@@ -209,7 +209,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
DSMRReaderSensorEntityDescription(
key="dsmr/consumption/gas/currently_delivered",
name="Current gas usage",
device_class=SensorDeviceClass.GAS,
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
state_class=SensorStateClass.MEASUREMENT,
),

View File

@@ -153,6 +153,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if self._device_info.uses_password:
return await self.async_step_authenticate()
self._password = ""
return self._async_get_entry()
async def async_step_discovery_confirm(

View File

@@ -72,15 +72,13 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
_attr_title = "ESPHome"
_attr_name = "Firmware"
_device_info: ESPHomeDeviceInfo
def __init__(
self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard
) -> None:
"""Initialize the update entity."""
super().__init__(coordinator=coordinator)
assert entry_data.device_info is not None
self._device_info = entry_data.device_info
self._entry_data = entry_data
self._attr_unique_id = entry_data.device_info.mac_address
self._attr_device_info = DeviceInfo(
connections={
@@ -88,6 +86,12 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
}
)
@property
def _device_info(self) -> ESPHomeDeviceInfo:
"""Return the device info."""
assert self._entry_data.device_info is not None
return self._entry_data.device_info
@property
def available(self) -> bool:
"""Return if update is available."""

View File

@@ -98,6 +98,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
for service_name in hass.services.async_services()[DOMAIN]:
hass.services.async_remove(DOMAIN, service_name)
if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False):
conversation.async_unset_agent(hass, entry)
return True

View File

@@ -3,7 +3,7 @@
"name": "Keenetic NDMS2 Router",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2",
"requirements": ["ndms2_client==0.1.1"],
"requirements": ["ndms2_client==0.1.2"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",

View File

@@ -3,7 +3,7 @@
"name": "Matter (BETA)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/matter",
"requirements": ["python-matter-server==2.0.1"],
"requirements": ["python-matter-server==2.0.2"],
"dependencies": ["websocket_api"],
"codeowners": ["@home-assistant/matter"],
"iot_class": "local_push"

View File

@@ -19,6 +19,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfElectricPotential,
UnitOfLength,
UnitOfTemperature,
)
@@ -41,7 +42,7 @@ SENSOR_DESCRIPTIONS = {
"battery_voltage": SensorEntityDescription(
key="battery_voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,

View File

@@ -13,7 +13,7 @@ from homeassistant import exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
@@ -41,37 +41,6 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp
return bridge.locks, bridge.openers
def _update_devices(devices: list[NukiDevice]) -> dict[str, set[str]]:
"""
Update the Nuki devices.
Returns:
A dict with the events to be fired. The event type is the key and the device ids are the value
"""
events: dict[str, set[str]] = defaultdict(set)
for device in devices:
for level in (False, True):
try:
if isinstance(device, NukiOpener):
last_ring_action_state = device.ring_action_state
device.update(level)
if not last_ring_action_state and device.ring_action_state:
events["ring"].add(device.nuki_id)
else:
device.update(level)
except RequestException:
continue
if device.state not in ERROR_STATES:
break
return events
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Nuki entry."""
@@ -101,42 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except RequestException as err:
raise exceptions.ConfigEntryNotReady from err
async def async_update_data() -> None:
"""Fetch data from Nuki bridge."""
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
async with async_timeout.timeout(10):
events = await hass.async_add_executor_job(
_update_devices, locks + openers
)
except InvalidCredentialsException as err:
raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err
except RequestException as err:
raise UpdateFailed(f"Error communicating with Bridge: {err}") from err
ent_reg = er.async_get(hass)
for event, device_ids in events.items():
for device_id in device_ids:
entity_id = ent_reg.async_get_entity_id(
Platform.LOCK, DOMAIN, device_id
)
event_data = {
"entity_id": entity_id,
"type": event,
}
hass.bus.async_fire("nuki_event", event_data)
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="nuki devices",
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=UPDATE_INTERVAL,
# Device registration for the bridge
info = bridge.info()
bridge_id = parse_id(info["ids"]["hardwareId"])
dev_reg = device_registry.async_get(hass)
dev_reg.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, bridge_id)},
manufacturer="Nuki Home Solutions GmbH",
name=f"Nuki Bridge {bridge_id}",
model="Hardware Bridge",
sw_version=info["versions"]["firmwareVersion"],
)
coordinator = NukiCoordinator(hass, bridge, locks, openers)
hass.data[DOMAIN][entry.entry_id] = {
DATA_COORDINATOR: coordinator,
DATA_BRIDGE: bridge,
@@ -178,3 +126,94 @@ class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]):
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._nuki_device = nuki_device
@property
def device_info(self):
"""Device info for Nuki entities."""
return {
"identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))},
"name": self._nuki_device.name,
"manufacturer": "Nuki Home Solutions GmbH",
"model": self._nuki_device.device_type_str.capitalize(),
"sw_version": self._nuki_device.firmware_version,
"via_device": (DOMAIN, self.coordinator.bridge_id),
}
class NukiCoordinator(DataUpdateCoordinator):
"""Data Update Coordinator for the Nuki integration."""
def __init__(self, hass, bridge, locks, openers):
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="nuki devices",
# Polling interval. Will only be polled if there are subscribers.
update_interval=UPDATE_INTERVAL,
)
self.bridge = bridge
self.locks = locks
self.openers = openers
@property
def bridge_id(self):
"""Return the parsed id of the Nuki bridge."""
return parse_id(self.bridge.info()["ids"]["hardwareId"])
async def _async_update_data(self) -> None:
"""Fetch data from Nuki bridge."""
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
async with async_timeout.timeout(10):
events = await self.hass.async_add_executor_job(
self.update_devices, self.locks + self.openers
)
except InvalidCredentialsException as err:
raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err
except RequestException as err:
raise UpdateFailed(f"Error communicating with Bridge: {err}") from err
ent_reg = entity_registry.async_get(self.hass)
for event, device_ids in events.items():
for device_id in device_ids:
entity_id = ent_reg.async_get_entity_id(
Platform.LOCK, DOMAIN, device_id
)
event_data = {
"entity_id": entity_id,
"type": event,
}
self.hass.bus.async_fire("nuki_event", event_data)
def update_devices(self, devices: list[NukiDevice]) -> dict[str, set[str]]:
"""
Update the Nuki devices.
Returns:
A dict with the events to be fired. The event type is the key and the device ids are the value
"""
events: dict[str, set[str]] = defaultdict(set)
for device in devices:
for level in (False, True):
try:
if isinstance(device, NukiOpener):
last_ring_action_state = device.ring_action_state
device.update(level)
if not last_ring_action_state and device.ring_action_state:
events["ring"].add(device.nuki_id)
else:
device.update(level)
except RequestException:
continue
if device.state not in ERROR_STATES:
break
return events

View File

@@ -34,13 +34,10 @@ async def async_setup_entry(
class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity):
"""Representation of a Nuki Lock Doorsensor."""
_attr_has_entity_name = True
_attr_name = "Door sensor"
_attr_device_class = BinarySensorDeviceClass.DOOR
@property
def name(self):
"""Return the name of the lock."""
return self._nuki_device.name
@property
def unique_id(self) -> str:
"""Return a unique ID."""

View File

@@ -67,13 +67,9 @@ async def async_setup_entry(
class NukiDeviceEntity(NukiEntity, LockEntity, ABC):
"""Representation of a Nuki device."""
_attr_has_entity_name = True
_attr_supported_features = LockEntityFeature.OPEN
@property
def name(self) -> str | None:
"""Return the name of the lock."""
return self._nuki_device.name
@property
def unique_id(self) -> str | None:
"""Return a unique ID."""

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from functools import partial
import logging
from typing import cast
import openai
from openai import error
@@ -13,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady, TemplateError
from homeassistant.helpers import area_registry, device_registry, intent, template
from homeassistant.helpers import area_registry, intent, template
from homeassistant.util import ulid
from .const import DEFAULT_MODEL, DEFAULT_PROMPT
@@ -74,6 +73,7 @@ class OpenAIAgent(conversation.AbstractConversationAgent):
try:
prompt = self._async_generate_prompt()
except TemplateError as err:
_LOGGER.error("Error rendering prompt: %s", err)
intent_response = intent.IntentResponse(language=user_input.language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
@@ -97,15 +97,26 @@ class OpenAIAgent(conversation.AbstractConversationAgent):
_LOGGER.debug("Prompt for %s: %s", model, prompt)
result = await self.hass.async_add_executor_job(
partial(
openai.Completion.create,
engine=model,
prompt=prompt,
max_tokens=150,
user=conversation_id,
try:
result = await self.hass.async_add_executor_job(
partial(
openai.Completion.create,
engine=model,
prompt=prompt,
max_tokens=150,
user=conversation_id,
)
)
)
except error.OpenAIError as err:
intent_response = intent.IntentResponse(language=user_input.language)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Sorry, I had a problem talking to OpenAI: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
_LOGGER.debug("Response %s", result)
response = result["choices"][0]["text"].strip()
self.history[conversation_id] = prompt + response
@@ -122,20 +133,9 @@ class OpenAIAgent(conversation.AbstractConversationAgent):
def _async_generate_prompt(self) -> str:
"""Generate a prompt for the user."""
dev_reg = device_registry.async_get(self.hass)
return template.Template(DEFAULT_PROMPT, self.hass).async_render(
{
"ha_name": self.hass.config.location_name,
"areas": [
area
for area in area_registry.async_get(self.hass).areas.values()
# Filter out areas without devices
if any(
not dev.disabled_by
for dev in device_registry.async_entries_for_area(
dev_reg, cast(str, area.id)
)
)
],
"areas": list(area_registry.async_get(self.hass).areas.values()),
}
)

View File

@@ -3,19 +3,26 @@
DOMAIN = "openai_conversation"
CONF_PROMPT = "prompt"
DEFAULT_MODEL = "text-davinci-003"
DEFAULT_PROMPT = """
You are a conversational AI for a smart home named {{ ha_name }}.
If a user wants to control a device, reject the request and suggest using the Home Assistant UI.
DEFAULT_PROMPT = """This smart home is controlled by Home Assistant.
An overview of the areas and the devices in this smart home:
{% for area in areas %}
{%- for area in areas %}
{%- set area_info = namespace(printed=false) %}
{%- for device in area_devices(area.name) -%}
{%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %}
{%- if not area_info.printed %}
{{ area.name }}:
{% for device in area_devices(area.name) -%}
{%- if not device_attr(device, "disabled_by") %}
- {{ device_attr(device, "name") }} ({{ device_attr(device, "model") }} by {{ device_attr(device, "manufacturer") }})
{%- endif %}
{%- set area_info.printed = true %}
{%- endif %}
- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %}
{%- endif %}
{%- endfor %}
{%- endfor %}
{% endfor %}
Answer the users questions about the world truthfully.
If the user wants to control a device, reject the request and suggest using the Home Assistant app.
Now finish this conversation:

View File

@@ -836,7 +836,9 @@ class Recorder(threading.Thread):
return
try:
shared_data_bytes = EventData.shared_data_bytes_from_event(event)
shared_data_bytes = EventData.shared_data_bytes_from_event(
event, self.dialect_name
)
except JSON_ENCODE_EXCEPTIONS as ex:
_LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex)
return
@@ -869,7 +871,7 @@ class Recorder(threading.Thread):
try:
dbstate = States.from_event(event)
shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event(
event, self._exclude_attributes_by_domain
event, self._exclude_attributes_by_domain, self.dialect_name
)
except JSON_ENCODE_EXCEPTIONS as ex:
_LOGGER.warning(

View File

@@ -43,11 +43,12 @@ from homeassistant.helpers.json import (
JSON_DECODE_EXCEPTIONS,
JSON_DUMP,
json_bytes,
json_bytes_strip_null,
json_loads,
)
import homeassistant.util.dt as dt_util
from .const import ALL_DOMAIN_EXCLUDE_ATTRS
from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect
from .models import StatisticData, StatisticMetaData, process_timestamp
# SQLAlchemy Schema
@@ -251,8 +252,12 @@ class EventData(Base): # type: ignore[misc,valid-type]
)
@staticmethod
def shared_data_bytes_from_event(event: Event) -> bytes:
def shared_data_bytes_from_event(
event: Event, dialect: SupportedDialect | None
) -> bytes:
"""Create shared_data from an event."""
if dialect == SupportedDialect.POSTGRESQL:
return json_bytes_strip_null(event.data)
return json_bytes(event.data)
@staticmethod
@@ -416,7 +421,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
@staticmethod
def shared_attrs_bytes_from_event(
event: Event, exclude_attrs_by_domain: dict[str, set[str]]
event: Event,
exclude_attrs_by_domain: dict[str, set[str]],
dialect: SupportedDialect | None,
) -> bytes:
"""Create shared_attrs from a state_changed event."""
state: State | None = event.data.get("new_state")
@@ -427,6 +434,10 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
exclude_attrs = (
exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS
)
if dialect == SupportedDialect.POSTGRESQL:
return json_bytes_strip_null(
{k: v for k, v in state.attributes.items() if k not in exclude_attrs}
)
return json_bytes(
{k: v for k, v in state.attributes.items() if k not in exclude_attrs}
)

View File

@@ -665,6 +665,7 @@ class SensorEntity(Entity):
(
"Entity %s (%s) is using native unit of measurement '%s' which "
"is not a valid unit for the device class ('%s') it is using; "
"expected one of %s; "
"Please update your configuration if your entity is manually "
"configured, otherwise %s"
),
@@ -672,6 +673,7 @@ class SensorEntity(Entity):
type(self),
native_unit_of_measurement,
device_class,
[str(unit) if unit else "no unit of measurement" for unit in units],
report_issue,
)

View File

@@ -513,7 +513,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None]
SensorDeviceClass.DATA_SIZE: set(SensorStateClass),
SensorDeviceClass.DATE: set(),
SensorDeviceClass.DISTANCE: set(SensorStateClass),
SensorDeviceClass.DURATION: set(),
SensorDeviceClass.DURATION: set(SensorStateClass),
SensorDeviceClass.ENERGY: {
SensorStateClass.TOTAL,
SensorStateClass.TOTAL_INCREASING,

View File

@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==5.3.0"],
"requirements": ["aioshelly==5.3.1"],
"dependencies": ["bluetooth", "http"],
"zeroconf": [
{

View File

@@ -17,7 +17,7 @@ button:
description: >-
Name of the button to press. Known possible values are
LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT,
MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN,
MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN,
PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
required: true
example: "LEFT"

View File

@@ -224,7 +224,8 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
"switch_type": False,
"button_delay": False,
"smart_bulb_mode": False,
"double_tap_up_for_full_brightness": True,
"double_tap_up_for_max_brightness": True,
"double_tap_down_for_min_brightness": True,
"led_color_when_on": True,
"led_color_when_off": True,
"led_intensity_when_on": True,

View File

@@ -7,7 +7,7 @@
"bellows==0.34.6",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.91",
"zha-quirks==0.0.92",
"zigpy-deconz==0.19.2",
"zigpy==0.53.0",
"zigpy-xbee==0.16.2",

View File

@@ -372,14 +372,26 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_
channel_names=CHANNEL_INOVELLI,
)
class InovelliDoubleTapForFullBrightness(
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_full_brightness"
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_max_brightness"
):
"""Inovelli double tap for full brightness control."""
_zcl_attribute: str = "double_tap_up_for_full_brightness"
_zcl_attribute: str = "double_tap_up_for_max_brightness"
_attr_name: str = "Double tap full brightness"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)
class InovelliDoubleTapForMinBrightness(
ZHASwitchConfigurationEntity, id_suffix="double_tap_down_for_min_brightness"
):
"""Inovelli double tap down for minimum brightness control."""
_zcl_attribute: str = "double_tap_down_for_min_brightness"
_attr_name: str = "Double tap minimum brightness"
@CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI,
)

View File

@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0b0"
PATCH_VERSION: Final = "0b2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View File

@@ -13,7 +13,7 @@ from collections.abc import Awaitable, Callable
import logging
import secrets
import time
from typing import Any, cast
from typing import Any, Optional, cast
from aiohttp import client, web
import async_timeout
@@ -437,7 +437,10 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView):
state = _decode_jwt(hass, request.query["state"])
if state is None:
return web.Response(text="Invalid state")
return web.Response(
text="Invalid state. Is My Home Assistant configured to go to the right instance?",
status=400,
)
user_input: dict[str, Any] = {"state": state}
@@ -538,7 +541,10 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str:
@callback
def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict | None:
"""JWT encode data."""
secret = cast(str, hass.data.get(DATA_JWT_SECRET))
secret = cast(Optional[str], hass.data.get(DATA_JWT_SECRET))
if secret is None:
return None
try:
return jwt.decode(encoded, secret, algorithms=["HS256"])

View File

@@ -71,6 +71,40 @@ def json_bytes(data: Any) -> bytes:
)
def json_bytes_strip_null(data: Any) -> bytes:
"""Dump json bytes after terminating strings at the first NUL."""
def process_dict(_dict: dict[Any, Any]) -> dict[Any, Any]:
"""Strip NUL from items in a dict."""
return {key: strip_null(o) for key, o in _dict.items()}
def process_list(_list: list[Any]) -> list[Any]:
"""Strip NUL from items in a list."""
return [strip_null(o) for o in _list]
def strip_null(obj: Any) -> Any:
"""Strip NUL from an object."""
if isinstance(obj, str):
return obj.split("\0", 1)[0]
if isinstance(obj, dict):
return process_dict(obj)
if isinstance(obj, list):
return process_list(obj)
return obj
# We expect null-characters to be very rare, hence try encoding first and look
# for an escaped null-character in the output.
result = json_bytes(data)
if b"\\u0000" in result:
# We work on the processed result so we don't need to worry about
# Home Assistant extensions which allows encoding sets, tuples, etc.
data_processed = orjson.loads(result)
data_processed = strip_null(data_processed)
result = json_bytes(data_processed)
return result
def json_dumps(data: Any) -> str:
"""Dump json string.

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2023.2.0b0"
version = "2023.2.0b2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@@ -267,7 +267,7 @@ aiosenseme==0.6.1
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==5.3.0
aioshelly==5.3.1
# homeassistant.components.skybell
aioskybell==22.7.0
@@ -1168,7 +1168,7 @@ mycroftapi==2.0
nad_receiver==0.3.0
# homeassistant.components.keenetic_ndms2
ndms2_client==0.1.1
ndms2_client==0.1.2
# homeassistant.components.ness_alarm
nessclient==0.10.0
@@ -2072,7 +2072,7 @@ python-kasa==0.5.0
# python-lirc==1.2.3
# homeassistant.components.matter
python-matter-server==2.0.1
python-matter-server==2.0.2
# homeassistant.components.xiaomi_miio
python-miio==0.5.12
@@ -2693,7 +2693,7 @@ zeroconf==0.47.1
zeversolar==0.2.0
# homeassistant.components.zha
zha-quirks==0.0.91
zha-quirks==0.0.92
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9

View File

@@ -245,7 +245,7 @@ aiosenseme==0.6.1
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==5.3.0
aioshelly==5.3.1
# homeassistant.components.skybell
aioskybell==22.7.0
@@ -867,7 +867,7 @@ mutagen==1.46.0
mutesync==0.0.1
# homeassistant.components.keenetic_ndms2
ndms2_client==0.1.1
ndms2_client==0.1.2
# homeassistant.components.ness_alarm
nessclient==0.10.0
@@ -1468,7 +1468,7 @@ python-juicenet==1.1.0
python-kasa==0.5.0
# homeassistant.components.matter
python-matter-server==2.0.1
python-matter-server==2.0.2
# homeassistant.components.xiaomi_miio
python-miio==0.5.12
@@ -1906,7 +1906,7 @@ zeroconf==0.47.1
zeversolar==0.2.0
# homeassistant.components.zha
zha-quirks==0.0.91
zha-quirks==0.0.92
# homeassistant.components.zha
zigpy-deconz==0.19.2

View File

@@ -518,6 +518,54 @@ async def test_reauth_fixed_via_dashboard(
assert len(mock_get_encryption_key.mock_calls) == 1
async def test_reauth_fixed_via_dashboard_remove_password(
hass, mock_client, mock_zeroconf, mock_dashboard
):
"""Test reauth fixed automatically via dashboard with password removed."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "hello",
CONF_DEVICE_NAME: "test",
},
)
entry.add_to_hass(hass)
mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test")
mock_dashboard["configured"].append(
{
"name": "test",
"configuration": "test.yaml",
}
)
await dashboard.async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_init(
"esphome",
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": entry.entry_id,
"unique_id": entry.unique_id,
},
)
assert result["type"] == FlowResultType.ABORT, result
assert result["reason"] == "reauth_successful"
assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK
assert entry.data[CONF_PASSWORD] == ""
assert len(mock_get_encryption_key.mock_calls) == 1
async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf):
"""Test reauth initiation with invalid PSK."""
entry = MockConfigEntry(

View File

@@ -1,14 +1,20 @@
"""Tests for the OpenAI integration."""
from unittest.mock import patch
from openai import error
from homeassistant.components import conversation
from homeassistant.core import Context
from homeassistant.helpers import device_registry
from homeassistant.helpers import area_registry, device_registry, intent
async def test_default_prompt(hass, mock_init_component):
"""Test that the default prompt works."""
device_reg = device_registry.async_get(hass)
area_reg = area_registry.async_get(hass)
for i in range(3):
area_reg.async_create(f"{i}Empty Area")
device_reg.async_get_or_create(
config_entry_id="1234",
@@ -18,12 +24,22 @@ async def test_default_prompt(hass, mock_init_component):
model="Test Model",
suggested_area="Test Area",
)
for i in range(3):
device_reg.async_get_or_create(
config_entry_id="1234",
connections={("test", f"{i}abcd")},
name="Test Service",
manufacturer="Test Manufacturer",
model="Test Model",
suggested_area="Test Area",
entry_type=device_registry.DeviceEntryType.SERVICE,
)
device_reg.async_get_or_create(
config_entry_id="1234",
connections={("test", "5678")},
name="Test Device 2",
manufacturer="Test Manufacturer 2",
model="Test Model 2",
model="Device 2",
suggested_area="Test Area 2",
)
device_reg.async_get_or_create(
@@ -31,29 +47,49 @@ async def test_default_prompt(hass, mock_init_component):
connections={("test", "9876")},
name="Test Device 3",
manufacturer="Test Manufacturer 3",
model="Test Model 3",
model="Test Model 3A",
suggested_area="Test Area 2",
)
device_reg.async_get_or_create(
config_entry_id="1234",
connections={("test", "qwer")},
name="Test Device 4",
suggested_area="Test Area 2",
)
device = device_reg.async_get_or_create(
config_entry_id="1234",
connections={("test", "9876-disabled")},
name="Test Device 3",
manufacturer="Test Manufacturer 3",
model="Test Model 3A",
suggested_area="Test Area 2",
)
device_reg.async_update_device(
device.id, disabled_by=device_registry.DeviceEntryDisabler.USER
)
with patch("openai.Completion.create") as mock_create:
await conversation.async_converse(hass, "hello", None, Context())
result = await conversation.async_converse(hass, "hello", None, Context())
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert (
mock_create.mock_calls[0][2]["prompt"]
== """You are a conversational AI for a smart home named test home.
If a user wants to control a device, reject the request and suggest using the Home Assistant UI.
== """This smart home is controlled by Home Assistant.
An overview of the areas and the devices in this smart home:
Test Area:
- Test Device (Test Model by Test Manufacturer)
- Test Device (Test Model)
Test Area 2:
- Test Device 2
- Test Device 3 (Test Model 3A)
- Test Device 4
- Test Device 2 (Test Model 2 by Test Manufacturer 2)
- Test Device 3 (Test Model 3 by Test Manufacturer 3)
Answer the users questions about the world truthfully.
If the user wants to control a device, reject the request and suggest using the Home Assistant app.
Now finish this conversation:
@@ -61,3 +97,12 @@ Smart home: How can I assist?
User: hello
Smart home: """
)
async def test_error_handling(hass, mock_init_component):
"""Test that the default prompt works."""
with patch("openai.Completion.create", side_effect=error.ServiceUnavailableError):
result = await conversation.async_converse(hass, "hello", None, Context())
assert result.response.response_type == intent.IntentResponseType.ERROR, result
assert result.response.error_code == "unknown", result

View File

@@ -34,6 +34,7 @@ from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import aliased, declarative_base, relationship
from sqlalchemy.orm.session import Session
from homeassistant.components.recorder.const import SupportedDialect
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_RESTORED,
@@ -287,7 +288,9 @@ class EventData(Base): # type: ignore[misc,valid-type]
)
@staticmethod
def shared_data_bytes_from_event(event: Event) -> bytes:
def shared_data_bytes_from_event(
event: Event, dialect: SupportedDialect | None
) -> bytes:
"""Create shared_data from an event."""
return json_bytes(event.data)
@@ -438,7 +441,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type]
@staticmethod
def shared_attrs_bytes_from_event(
event: Event, exclude_attrs_by_domain: dict[str, set[str]]
event: Event,
exclude_attrs_by_domain: dict[str, set[str]],
dialect: SupportedDialect | None,
) -> bytes:
"""Create shared_attrs from a state_changed event."""
state: State | None = event.data.get("new_state")

View File

@@ -31,6 +31,7 @@ from homeassistant.components.recorder.const import (
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
EVENT_RECORDER_HOURLY_STATISTICS_GENERATED,
KEEPALIVE_TIME,
SupportedDialect,
)
from homeassistant.components.recorder.db_schema import (
SCHEMA_VERSION,
@@ -223,6 +224,42 @@ async def test_saving_state(recorder_mock, hass: HomeAssistant):
assert state == _state_with_context(hass, entity_id)
@pytest.mark.parametrize(
"dialect_name, expected_attributes",
(
(SupportedDialect.MYSQL, {"test_attr": 5, "test_attr_10": "silly\0stuff"}),
(SupportedDialect.POSTGRESQL, {"test_attr": 5, "test_attr_10": "silly"}),
(SupportedDialect.SQLITE, {"test_attr": 5, "test_attr_10": "silly\0stuff"}),
),
)
async def test_saving_state_with_nul(
recorder_mock, hass: HomeAssistant, dialect_name, expected_attributes
):
"""Test saving and restoring a state with nul in attributes."""
entity_id = "test.recorder"
state = "restoring_from_db"
attributes = {"test_attr": 5, "test_attr_10": "silly\0stuff"}
with patch(
"homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name
):
hass.states.async_set(entity_id, state, attributes)
await async_wait_recording_done(hass)
with session_scope(hass=hass) as session:
db_states = []
for db_state, db_state_attributes in session.query(States, StateAttributes):
db_states.append(db_state)
state = db_state.to_native()
state.attributes = db_state_attributes.to_native()
assert len(db_states) == 1
assert db_states[0].event_id is None
expected = _state_with_context(hass, entity_id)
expected.attributes = expected_attributes
assert state == expected
async def test_saving_many_states(
async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant
):

View File

@@ -9,7 +9,11 @@ import pytest
from pytest import approx
from homeassistant.components.number import NumberDeviceClass
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.components.sensor import (
DEVICE_CLASS_UNITS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
PERCENTAGE,
@@ -1403,13 +1407,17 @@ async def test_device_classes_with_invalid_unit_of_measurement(
device_class=device_class,
native_unit_of_measurement="INVALID!",
)
units = [
str(unit) if unit else "no unit of measurement"
for unit in DEVICE_CLASS_UNITS.get(device_class, set())
]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
assert (
"is using native unit of measurement 'INVALID!' which is not a valid "
f"unit for the device class ('{device_class}') it is using"
f"unit for the device class ('{device_class}') it is using; "
f"expected one of {units}"
) in caplog.text

View File

@@ -726,3 +726,10 @@ async def test_oauth_session_refresh_failure(
session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl)
with pytest.raises(aiohttp.client_exceptions.ClientResponseError):
await session.async_request("post", "https://example.com")
async def test_oauth2_without_secret_init(local_impl, hass_client_no_auth):
"""Check authorize callback without secret initalizated."""
client = await hass_client_no_auth()
resp = await client.get("/auth/external/callback?code=abcd&state=qwer")
assert resp.status == 400

View File

@@ -10,6 +10,7 @@ from homeassistant import core
from homeassistant.helpers.json import (
ExtendedJSONEncoder,
JSONEncoder,
json_bytes_strip_null,
json_dumps,
json_dumps_sorted,
)
@@ -118,3 +119,19 @@ def test_json_dumps_rgb_color_subclass():
rgb = RGBColor(4, 2, 1)
assert json_dumps(rgb) == "[4,2,1]"
def test_json_bytes_strip_null():
"""Test stripping nul from strings."""
assert json_bytes_strip_null("\0") == b'""'
assert json_bytes_strip_null("silly\0stuff") == b'"silly"'
assert json_bytes_strip_null(["one", "two\0", "three"]) == b'["one","two","three"]'
assert (
json_bytes_strip_null({"k1": "one", "k2": "two\0", "k3": "three"})
== b'{"k1":"one","k2":"two","k3":"three"}'
)
assert (
json_bytes_strip_null([[{"k1": {"k2": ["silly\0stuff"]}}]])
== b'[[{"k1":{"k2":["silly"]}}]]'
)