7 Commits
v0.3.3 ... main

Author SHA1 Message Date
ted
e5165f34da fix: Fix the HA warning in the logs related to vacuum state setting (#694)
Some checks failed
Tests / check-rule-format (push) Failing after 12s
Validate / validate-hassfest (push) Failing after 8s
Validate / validate-hacs (push) Failing after 3m6s
Validate / validate-lint (push) Failing after 11s
Validate / validate-setup (push) Failing after 9s
2025-07-08 13:48:17 +08:00
9fbbb26d33 fix: translation it.json (#1215) 2025-07-08 13:46:36 +08:00
5b1d003bb2 feat: subscribe the BLE device up messages even though the device is offline (#1207)
Some checks failed
Tests / check-rule-format (push) Failing after 10s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 11s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 5s
* feat: subscribe the BLE device up messages even though the device is offline (#1170)

* fix: set all BLE devices online
2025-06-30 11:27:12 +08:00
6069eaaba8 feat: exclude unsupported model (#1205)
* feat: ignore unsupported models (#933)

* fix: remove unnecessary logs
2025-06-30 11:12:58 +08:00
fd57e7c565 fix: reconnect delay time (#1200)
* fix: reset the reconnect interval when connected (#1175)

* feat: set the default reconnect delay time as 10 seconds

* fix: get the minimum reconnect interval
2025-06-30 11:12:18 +08:00
096b33f3c9 fix: the operation mode when the device does not have a mode property (#1199) 2025-06-30 11:11:36 +08:00
664787ca58 fix: ptx air-conditioner environment temperature (#1210)
Some checks failed
Tests / check-rule-format (push) Failing after 6s
Validate / validate-hassfest (push) Failing after 5s
Validate / validate-hacs (push) Failing after 13s
Validate / validate-lint (push) Failing after 4s
Validate / validate-setup (push) Failing after 7s
* fix: ptx air-conditioner temperature #1163

* fix: environment temperature siid and piid
2025-06-27 17:52:39 +08:00
10 changed files with 158 additions and 45 deletions

View File

@ -85,6 +85,11 @@ SUPPORTED_PLATFORMS: list = [
'water_heater',
]
UNSUPPORTED_MODELS: list = [
'chuangmi.ir.v2',
'xiaomi.router.rd03'
]
DEFAULT_CLOUD_SERVER: str = 'cn'
CLOUD_SERVERS: dict = {
'cn': '中国大陆',

View File

@ -1354,6 +1354,11 @@ class MIoTClient:
"""Update cloud devices.
NOTICE: This function will operate the cloud_list
"""
# MIoT cloud service may not publish the online state updating message
# for the BLE device. Assume that all BLE devices are online.
for did, info in cloud_list.items():
if did.startswith('blt.'):
info['online'] = True
for did, info in self._device_list_cache.items():
if filter_dids and did not in filter_dids:
continue

View File

@ -59,6 +59,7 @@ import aiohttp
# pylint: disable=relative-beyond-top-level
from .common import calc_group_id
from .const import (
UNSUPPORTED_MODELS,
DEFAULT_OAUTH2_API_HOST,
MIHOME_HTTP_API_TIMEOUT,
OAUTH2_AUTH_URL)
@ -573,6 +574,10 @@ class MIoTHttpClient:
# were implemented.
_LOGGER.info('ignore miwifi.* device, cloud, %s', did)
continue
if model in UNSUPPORTED_MODELS:
_LOGGER.info('ignore unsupported model %s, cloud, %s',
model, did)
continue
device_infos[did] = {
'did': did,
'uid': device.get('uid', None),

View File

@ -1207,10 +1207,9 @@ class MIoTPropertyEntity(Entity):
self._attr_available = miot_device.online
_LOGGER.info(
'new miot property entity, %s, %s, %s, %s, %s, %s, %s',
'new miot property entity, %s, %s, %s, %s, %s',
self.miot_device.name, self._attr_name, spec.platform,
spec.device_class, self.entity_id, self._value_range,
self._value_list)
spec.device_class, self.entity_id)
@property
def device_info(self) -> Optional[DeviceInfo]:

View File

@ -68,7 +68,7 @@ from paho.mqtt.client import (
# pylint: disable=relative-beyond-top-level
from .common import MIoTMatcher
from .const import MIHOME_MQTT_KEEPALIVE
from .const import UNSUPPORTED_MODELS, MIHOME_MQTT_KEEPALIVE
from .miot_error import MIoTErrorCode, MIoTMipsError
_LOGGER = logging.getLogger(__name__)
@ -216,7 +216,7 @@ class _MipsClient(ABC):
MQTT_INTERVAL_S = 1
MIPS_QOS: int = 2
UINT32_MAX: int = 0xFFFFFFFF
MIPS_RECONNECT_INTERVAL_MIN: float = 30
MIPS_RECONNECT_INTERVAL_MIN: float = 10
MIPS_RECONNECT_INTERVAL_MAX: float = 600
MIPS_SUB_PATCH: int = 300
MIPS_SUB_INTERVAL: float = 1
@ -641,6 +641,7 @@ class _MipsClient(ABC):
if not self._mqtt.is_connected():
return
self.log_info(f'mips connect, {flags}, {rc}, {props}')
self.__reset_reconnect_time()
self._mqtt_state = True
self._internal_loop.call_soon(
self._on_mips_connect, rc, props)
@ -822,7 +823,7 @@ class _MipsClient(ABC):
self._internal_loop.stop()
def __get_next_reconnect_time(self) -> float:
if self._mips_reconnect_interval == 0:
if self._mips_reconnect_interval < self.MIPS_RECONNECT_INTERVAL_MIN:
self._mips_reconnect_interval = self.MIPS_RECONNECT_INTERVAL_MIN
else:
self._mips_reconnect_interval = min(
@ -830,6 +831,9 @@ class _MipsClient(ABC):
self.MIPS_RECONNECT_INTERVAL_MAX)
return self._mips_reconnect_interval
def __reset_reconnect_time(self) -> None:
self._mips_reconnect_interval = 0
class MipsCloudClient(_MipsClient):
"""MIoT Pub/Sub Cloud Client."""
@ -1361,6 +1365,9 @@ class MipsLocalClient(_MipsClient):
if name is None or urn is None or model is None:
self.log_error(f'invalid device info, {did}, {info}')
continue
if model in UNSUPPORTED_MODELS:
self.log_info(f'unsupported model, {model}, {did}')
continue
device_list[did] = {
'did': did,
'name': name,

View File

@ -1,4 +1,29 @@
{
"urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:1": [
{
"iid": 4,
"type": "urn:miot-spec-v2:service:environment:0000780A:090615-ktf:1",
"description": "Environment",
"properties": [
{
"iid": 2,
"type": "urn:miot-spec-v2:property:temperature:00000020:090615-ktf:1",
"description": "Temperature",
"format": "float",
"access": [
"read",
"notify"
],
"unit": "celsius",
"value-range": [
-30,
100,
1
]
}
]
}
],
"urn:miot-spec-v2:device:airer:0000A00D:hyd-lyjpro:1": [
{
"iid": 3,

View File

@ -1,3 +1,6 @@
urn:miot-spec-v2:device:air-conditioner:0000A004:090615-ktf:
services:
- '4'
urn:miot-spec-v2:device:air-purifier:0000A007:zhimi-ma4:
properties:
- 9.*
@ -41,9 +44,6 @@ urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:
services:
- '1'
- '5'
urn:miot-spec-v2:device:router:0000A036:xiaomi-rd03:
services:
- '*'
urn:miot-spec-v2:device:thermostat:0000A031:tofan-wk01:
services:
- '2'

View File

@ -113,7 +113,7 @@
},
"config_options": {
"title": "Opzioni di Configurazione",
"description": "### Ciao, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegione di Login Corrente: {cloud_server}\r\n\r\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.",
"description": "### Ciao, {nick_name}\r\n\r\nID Xiaomi: {uid}\r\nRegione di Login Corrente: {cloud_server}\r\nID istanza di integrazione: {instance_id}\r\n\r\nSeleziona le opzioni che desideri configurare, poi clicca AVANTI.",
"data": {
"integration_language": "Lingua dell'Integrazione",
"update_user_info": "Aggiorna le informazioni dell'utente",

View File

@ -47,29 +47,26 @@ Vacuum entities for Xiaomi Home.
"""
from __future__ import annotations
from typing import Any, Optional
import re
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.components.vacuum import (
StateVacuumEntity,
VacuumEntityFeature
)
from homeassistant.components.vacuum import (StateVacuumEntity,
VacuumEntityFeature)
from .miot.const import DOMAIN
from .miot.miot_device import MIoTDevice, MIoTServiceEntity, MIoTEntityData
from .miot.miot_spec import (
MIoTSpecAction,
MIoTSpecProperty)
from .miot.miot_spec import (MIoTSpecAction, MIoTSpecProperty)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
device_list: list[MIoTDevice] = hass.data[DOMAIN]['devices'][
config_entry.entry_id]
@ -99,10 +96,12 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
_status_map: Optional[dict[int, str]]
_fan_level_map: Optional[dict[int, str]]
def __init__(
self, miot_device: MIoTDevice, entity_data: MIoTEntityData
) -> None:
_device_name: str
def __init__(self, miot_device: MIoTDevice,
entity_data: MIoTEntityData) -> None:
super().__init__(miot_device=miot_device, entity_data=entity_data)
self._device_name = miot_device.name
self._attr_supported_features = VacuumEntityFeature(0)
self._prop_status = None
@ -121,21 +120,21 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
for prop in entity_data.props:
if prop.name == 'status':
if not prop.value_list:
_LOGGER.error(
'invalid status value_list, %s', self.entity_id)
_LOGGER.error('invalid status value_list, %s',
self.entity_id)
continue
self._status_map = prop.value_list.to_map()
self._attr_supported_features |= VacuumEntityFeature.STATE
self._prop_status = prop
elif prop.name == 'fan-level':
if not prop.value_list:
_LOGGER.error(
'invalid fan-level value_list, %s', self.entity_id)
_LOGGER.error('invalid fan-level value_list, %s',
self.entity_id)
continue
self._fan_level_map = prop.value_list.to_map()
self._attr_fan_speed_list = list(self._fan_level_map.values())
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
self._prop_fan_level = prop
elif prop.name == 'battery-level':
self._attr_supported_features |= VacuumEntityFeature.BATTERY
self._prop_battery_level = prop
@ -155,16 +154,24 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
elif action.name == 'stop-and-gocharge':
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
self._action_stop_and_gocharge = action
elif action.name == 'identify':
self._attr_supported_features |= VacuumEntityFeature.LOCATE
self._action_identify = action
async def async_start(self) -> None:
"""Start or resume the cleaning task."""
if self.state.lower() in ['paused', '暂停中']:
await self.action_async(action=self._action_continue_sweep)
return
try: # VacuumActivity is introduced in HA core 2025.1.0
# pylint: disable=import-outside-toplevel
from homeassistant.components.vacuum import VacuumActivity
if (self.activity
== VacuumActivity.PAUSED) and self._action_continue_sweep:
await self.action_async(action=self._action_continue_sweep)
return
except ImportError:
if self.state and (self.state in {'paused', 'pause'
}) and self._action_continue_sweep:
await self.action_async(action=self._action_continue_sweep)
return
await self.action_async(action=self._action_start_sweep)
async def async_stop(self, **kwargs: Any) -> None:
@ -179,31 +186,92 @@ class Vacuum(MIoTServiceEntity, StateVacuumEntity):
"""Set the vacuum cleaner to return to the dock."""
await self.action_async(action=self._action_stop_and_gocharge)
async def async_clean_spot(self, **kwargs: Any) -> None:
"""Perform a spot clean-up."""
async def async_locate(self, **kwargs: Any) -> None:
"""Locate the vacuum cleaner."""
await self.action_async(action=self._action_identify)
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""Set fan speed."""
fan_level_value = self.get_map_key(map_=self._fan_level_map,
value=fan_speed)
await self.set_property_async(prop=self._prop_fan_level,
value=fan_level_value)
@property
def name(self) -> Optional[str]:
"""Name of the vacuum entity."""
return self._device_name
@property
def state(self) -> Optional[str]:
"""Return the current state of the vacuum cleaner."""
return self.get_map_value(
map_=self._status_map,
key=self.get_prop_value(prop=self._prop_status))
"""Return the current state of the vacuum cleaner.
To fix the HA warning below:
Detected that custom integration 'xiaomi_home' is setting state
directly.Entity XXX(<class 'custom_components.xiaomi_home.vacuum
.Vacuum'>)should implement the 'activity' property and return
its state using the VacuumActivity enum.This will stop working in
Home Assistant 2026.1.
Refer to
https://developers.home-assistant.io/blog/2024/12/08/new-vacuum-state-property
There are only 6 states in VacuumActivity enum. To be compatible with
more constants, try get matching VacuumActivity enum first, return state
string as before if there is no match. In Home Assistant 2026.1, every
state should map to a VacuumActivity enum.
"""
return self.activity
@property
def activity(self) -> Optional[str]:
"""The current vacuum activity."""
status = self.get_prop_value(prop=self._prop_status)
if status is None:
return None
status_value = self.get_map_value(map_=self._status_map, key=status)
if status_value is None:
return None
try:
# pylint: disable=import-outside-toplevel
from homeassistant.components.vacuum import VacuumActivity
status_value = status_value.lower()
status_str = re.sub(r'[^a-z]', '', status_value)
if status_str in {
'charging', 'charged', 'chargingcompleted', 'fullcharge',
'fullpower', 'findchargerpause', 'drying', 'washing',
'wash', 'inthewash', 'inthedry', 'stationworking',
'dustcollecting', 'upgrade', 'upgrading', 'updating'
}:
return VacuumActivity.DOCKED
if status_str in {'paused', 'pause'}:
return VacuumActivity.PAUSED
if status_str in {
'gocharging', 'cleancompletegocharging', 'findchargewash',
'backtowashmop', 'gowash', 'gowashing', 'summon'
}:
return VacuumActivity.RETURNING
if (status_str.find('sweeping')
!= -1) or (status_str.find('mopping')
!= -1) or (status_str in {
'cleaning', 'remoteclean', 'continuesweep',
'busy', 'building', 'buildingmap', 'mapping'
}):
return VacuumActivity.CLEANING
if status_str in {'error', 'breakcharging', 'gochargebreak'}:
return VacuumActivity.ERROR
return VacuumActivity.IDLE
except ImportError:
return status_value
@property
def battery_level(self) -> Optional[int]:
"""Return the current battery level of the vacuum cleaner."""
"""The current battery level of the vacuum cleaner."""
return self.get_prop_value(prop=self._prop_battery_level)
@property
def fan_speed(self) -> Optional[str]:
"""Return the current fan speed of the vacuum cleaner."""
"""The current fan speed of the vacuum cleaner."""
return self.get_map_value(
map_=self._fan_level_map,
key=self.get_prop_value(prop=self._prop_fan_level))

View File

@ -141,12 +141,11 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
continue
self._mode_map = prop.value_list.to_map()
self._attr_operation_list = list(self._mode_map.values())
self._attr_supported_features |= (
WaterHeaterEntityFeature.OPERATION_MODE)
self._prop_mode = prop
if not self._attr_operation_list:
self._attr_operation_list = [STATE_ON]
self._attr_operation_list.append(STATE_OFF)
self._attr_supported_features |= WaterHeaterEntityFeature.OPERATION_MODE
async def async_turn_on(self) -> None:
"""Turn the water heater on."""
@ -197,5 +196,5 @@ class WaterHeater(MIoTServiceEntity, WaterHeaterEntity):
return STATE_OFF
if not self._prop_mode and self.get_prop_value(prop=self._prop_on):
return STATE_ON
return self.get_map_value(map_=self._mode_map,
key=self.get_prop_value(prop=self._prop_mode))
return (None if self._prop_mode is None else self.get_map_value(
map_=self._mode_map, key=self.get_prop_value(prop=self._prop_mode)))