Китайские электрические Терморегуляторы/Термостаты с ZigBee-сетями
Этих устройств, очевидно, наштамповано великое множество. и у всех них свои model + manufacturer идентификаторы.
Я не знаю зачем там лепят некоторые "немецкое качество" - от любого настоящего EU-устройства эти отличаются как небо от земли...
Приятный момент: в молочном и темном (черном) исполнении эти штуки выглядят довольно таки стильно-модно-молодежно и хорошо в интерьерах смотрятся! Так-же в подрозетники стандартные круглые 68мм влезают на ура. Цифры и пиктограмки на лицевых панелях хорошего размера и видны глазам без присмотра. Они в меру яркие, четкие и читаемые. Управление сенсорное 5ти кнопками и довольно легкое. справится любой, кроме работы с расписанием, с ним что-то очень муторно и сложно вышло в интерфейсном плане.
Габаритные размеры представлены на картинке ниже
Те, что я сам держал в руках:
- Electsmart EST-111Z. Коробка светло-синяя со стикером модели. Внутри лежит: два винтика, инструкция (очень кривая и переведенная, похоже, машинным переводом БЕЗ какой-либо ручной вычитки), термодатчик на длином проводе и сам девайс. В моем варианте это "сухой контакт" 5А реле. Удивительно что даже в этих одинаковых коробках (а я брал много их) нашлись целых два идентификатора различных у одинаковых моделей: TS0601 _TZE200_edl8pz1k и TS0601 _TZE204_edl8pz1k
- MOES BHT-006. Вроде как китайская "фирма брэнд". Внутри: только сам девайс, инструкция на английском где не различают wifi и zigbee, два винта. Идентификатор устройства TS0601 _TZE204_aoclfnxz. (Внутри - абсолютно полная копия ElectSmart EST-1111Z. Разница только в наклейках дат производства 2023 у electsmart и 2024 у MOES). Документация красочная печатная со вменяемым описанием опций.
- Нечто безымянное TW Smart Thermostat. С дисплеем вместо светодиодного интерфейса. (но повторяющим полностью паттерн взаимодействия с предыдущими). (Будущая, отдельная статья)
И есть те что не держал но очень похожи визуально и органами управления (Огромное спасибо передавшим мне фотографии!):
(01.12.2024) модель MOES BHT-002. Маркировка печатной платы 002_WIFI_20220426_V4
Благодаря зацепке в присланной фотографии, можно вероятнее всего "угадать" используемую в остальных устройствах микросхему центрального контроллера. Там, где она со спиленой маркировкой. Итак производитель - Китайская компания Jin Rui которая под именем CACHIP работает. Приведу цитатату из источника:
"Shenzhen based design company Jinrui offers modules for consumer electronics, home audio and car audio applications under their CACHIP brand."
В, интересующее нас, семейство 8ми битных микроконтроллеров (с совместимой 1T8051 системой команд) входят модели CA51F2x в корпусе LQFP64 по своим ТТХ, очень похоже используются в этих термостататах. Семейства чипов CA51F1 и F3 не подходят потому что корпусовка у них совсем не та. значит "выбор" у нас из 3х моделей CA51F251L3, CA51F252L3, CA51F253L3. именно они 64 ножные и различаются только обьемом флеш памяти контроллера 8/16/32кб. Документацию можно посмотреть например вот здесь. Тактовая частота (при внешнем кварце как у нас) может достигать 24Мгц. 8кб до 32кб флеш памяти и до 2кб ОЗУ. Для прошивки, по всей видимости, требуется некая фирмачевая утилита от производителя CACHIP_TOOL_X.X.X.exe где Х это разные цифры номеров билдов.
Кстати, данная утилита умеет только заливать прошивку, проверять и некие отладочные фишки.
Хотя открыв документацию на MCU, там описывается что можно считывать содержимое прошивки и управлять блокировкой.
Кому интересно больше подробностей про сами МК и утилиту прошивки а так-же программатор, загляните в мою соседнюю статью.
На момент начала написания данной заметки (20.10.2024) все они внутри Home Assistant (а именно интеграции ZHA) видны как просто устройства-роутеры. т.е. сам по себе HA управлять ими как термостатами "умными" не даст без zha quirk либо z2m-конвертора (если через mqtt зацеплен). В этой связи для работы в HA мною был изготовлен "базовый" zha quirk который подходит к первой модели из списка. Он не умеет пока в расписание т.к. я не до конца разобрался еще с форматами данных и тем как строится zha quirk. Вообще - то как реализованы zha quirks это отдельный ад архитектуры. Нет ни описания толкового , ни примеров и как оказалось большая часть там "прибивается гвоздями" наглухо из контролов. Но это тема отдельной статьи...
"""Map from manufacturer to standard clusters for electric heating thermostats."""
import logging
import zigpy.types as t
from typing import Optional, Union
from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
)
from zhaquirks.tuya import (
EnchantedDevice,
TuyaManufCluster,
TuyaManufClusterAttributes,
TuyaPowerConfigurationCluster,
TuyaThermostat,
TuyaThermostatCluster,
TuyaTimePayload,
TuyaUserInterfaceCluster,
)
from zigpy.profiles import zha
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import (
AnalogOutput,
Basic,
GreenPowerProxy,
Groups,
OnOff,
Ota,
Scenes,
Time,
)
from zigpy.zcl.clusters.hvac import Thermostat
ELECTSMART_TARGET_TEMP_ATTR = 0x0210 # [0,0,0,21] target room temp (degree)
ELECTSMART_TEMPERATURE_ATTR = 0x0218 # [0,0,0,200] current room temp (decidegree)
ELECTSMART_EXTERNAL_TEMPERATURE_ATTR = 0x0266 # available in OUT,AL modes. IN = always zero
ELECTSMART_MODE_ATTR = 0x0402 # [0] manual [1] schedule
ELECTSMART_SYSTEM_MODE_ATTR = 0x0101 # [0] off [1] on
ELECTSMART_SENSOR_ATTR = 0x042B # sensor
ELECTSMART_HEAT_STATE_ATTR = 0x0424 # [1] idle [0] heating /!\ inverted
ELECTSMART_CHILD_LOCK_ATTR = 0x0128 # [0] unlocked [1] child-locked
ELECTSMART_TEMP_CALIBRATION_ATTR = 0x021B # temperature calibration (degree)
ELECTSMART_MIN_TEMPERATURE_ATTR = 0x021A # min setpoint temp
ELECTSMART_MAX_TEMPERATURE_ATTR = 0x0212 # max setpoint temp
ELECTSMART_MIN_TEMPERATURE_VAL = 50 # minimum limit of temperature setting (degree/10)
ELECTSMART_MAX_TEMPERATURE_VAL = 350 # maximum limit of temperature setting (degree/10)
ElectsmartManufClusterSelf = {}
_LOGGER = logging.getLogger(__name__)
class CustomTuyaOnOff(LocalDataCluster, OnOff):
"""Custom Tuya OnOff cluster."""
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.thermostat_onoff_bus.add_listener(self)
# pylint: disable=R0201
def map_attribute(self, attribute, value):
"""Map standardized attribute value to dict of manufacturer values."""
return {}
async def write_attributes(self, attributes, manufacturer=None):
"""Implement writeable attributes."""
records = self._write_attr_records(attributes)
if not records:
return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]
manufacturer_attrs = {}
for record in records:
attr_name = self.attributes[record.attrid].name
new_attrs = self.map_attribute(attr_name, record.value.value)
_LOGGER.debug(
"[0x%04x:%s:0x%04x] Mapping standard %s (0x%04x) "
"with value %s to custom %s",
self.endpoint.device.nwk,
self.endpoint.endpoint_id,
self.cluster_id,
attr_name,
record.attrid,
repr(record.value.value),
repr(new_attrs),
)
manufacturer_attrs.update(new_attrs)
if not manufacturer_attrs:
return [
[
foundation.WriteAttributesStatusRecord(
foundation.Status.FAILURE, r.attrid
)
for r in records
]
]
await ElectsmartManufClusterSelf[
self.endpoint.device.ieee
].endpoint.tuya_manufacturer.write_attributes(
manufacturer_attrs, manufacturer=manufacturer
)
return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]
async def command(
self,
command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
*args,
manufacturer: Optional[Union[int, t.uint16_t]] = None,
expect_reply: bool = True,
tsn: Optional[Union[int, t.uint8_t]] = None,
):
"""Override the default Cluster command."""
if command_id in (0x0000, 0x0001, 0x0002):
if command_id == 0x0000:
value = False
elif command_id == 0x0001:
value = True
else:
attrid = self.attributes_by_name["on_off"].id
success, _ = await self.read_attributes(
(attrid,), manufacturer=manufacturer
)
try:
value = success[attrid]
except KeyError:
return foundation.Status.FAILURE
value = not value
(res,) = await self.write_attributes(
{"on_off": value},
manufacturer=manufacturer,
)
return [command_id, res[0].status]
return [command_id, foundation.Status.UNSUP_CLUSTER_COMMAND]
class ElectsmartManufCluster(TuyaManufClusterAttributes):
"""Manufacturer Specific Cluster of some electric heating thermostats."""
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
global ElectsmartManufClusterSelf
ElectsmartManufClusterSelf[self.endpoint.device.ieee] = self
set_time_offset = 1970
server_commands = {
0x0000: foundation.ZCLCommandDef(
"set_data",
{"param": TuyaManufCluster.Command},
False,
is_manufacturer_specific=False,
),
0x0010: foundation.ZCLCommandDef(
"mcu_version_req",
{"param": t.uint16_t},
False,
is_manufacturer_specific=True,
),
0x0024: foundation.ZCLCommandDef(
"set_time",
{"param": TuyaTimePayload},
False,
is_manufacturer_specific=False,
),
}
attributes = TuyaManufClusterAttributes.attributes.copy()
attributes.update(
{
ELECTSMART_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
ELECTSMART_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
ELECTSMART_EXTERNAL_TEMPERATURE_ATTR: ("external_temperature", t.uint32_t, True),
ELECTSMART_MODE_ATTR: ("mode", t.uint8_t, True),
ELECTSMART_SYSTEM_MODE_ATTR: ("system_mode", t.uint8_t, True),
ELECTSMART_HEAT_STATE_ATTR: ("heat_state", t.uint8_t, True),
ELECTSMART_SENSOR_ATTR: ("sensor_choose", t.uint8_t, True),
ELECTSMART_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
ELECTSMART_MIN_TEMPERATURE_ATTR: ("min_temperature", t.uint32_t, True),
ELECTSMART_MAX_TEMPERATURE_ATTR: ("max_temperature", t.uint32_t, True),
ELECTSMART_TEMP_CALIBRATION_ATTR: ("temperature_calibration", t.int32s, True),
}
)
DIRECT_MAPPED_ATTRS = {
ELECTSMART_TEMPERATURE_ATTR: (
"local_temperature",
lambda value: value * 10,
),
ELECTSMART_EXTERNAL_TEMPERATURE_ATTR: (
"external_temperature",
lambda value: value * 10,
),
ELECTSMART_TARGET_TEMP_ATTR: (
"occupied_heating_setpoint",
lambda value: value * 10,
),
ELECTSMART_MIN_TEMPERATURE_ATTR: (
"min_heat_setpoint_limit",
lambda value: value * 10,
),
ELECTSMART_MAX_TEMPERATURE_ATTR: (
"max_heat_setpoint_limit",
lambda value: value * 10,
),
}
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid in self.DIRECT_MAPPED_ATTRS:
self.endpoint.device.thermostat_bus.listener_event(
"temperature_change",
self.DIRECT_MAPPED_ATTRS[attrid][0],
(
value
if self.DIRECT_MAPPED_ATTRS[attrid][1] is None
else self.DIRECT_MAPPED_ATTRS[attrid][1](value)
),
)
elif attrid == ELECTSMART_TEMP_CALIBRATION_ATTR:
self.endpoint.device.ElectsmartTempCalibration_bus.listener_event("set_value", value)
elif attrid == ELECTSMART_CHILD_LOCK_ATTR:
self.endpoint.device.ui_bus.listener_event("child_lock_change", value)
self.endpoint.device.thermostat_onoff_bus.listener_event("child_lock_change", value)
elif attrid == ELECTSMART_MODE_ATTR:
self.endpoint.device.thermostat_bus.listener_event("mode_change", value)
elif attrid == ELECTSMART_SYSTEM_MODE_ATTR:
self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
elif attrid == ELECTSMART_HEAT_STATE_ATTR:
self.endpoint.device.thermostat_bus.listener_event("state_change", 1 - value)
class ElectsmartThermostat(TuyaThermostatCluster):
"""Thermostat cluster for some electric heating controllers."""
class Preset(t.enum8):
"""Working modes of the thermostat."""
Schedule = 0x00
Manual = 0x01
class SensorSource(t.enum8):
"""Working modes of the thermostat."""
IN = 0x00
AL = 0x01
OU = 0x02
_CONSTANT_ATTRIBUTES = {
0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only,
}
attributes = TuyaThermostatCluster.attributes.copy()
attributes.update(
{
ELECTSMART_SENSOR_ATTR: ("sensor_choose", SensorSource, True),
0x4002: ("operation_preset", Preset, True),
}
)
DIRECT_MAPPING_ATTRS = {
"occupied_heating_setpoint": (
ELECTSMART_TARGET_TEMP_ATTR,
lambda value: round(value / 10),
),
"min_heat_setpoint_limit": (
ELECTSMART_MIN_TEMPERATURE_ATTR,
lambda value: round(value / 10),
),
"max_heat_setpoint_limit": (
ELECTSMART_MAX_TEMPERATURE_ATTR,
lambda value: round(value / 10),
),
"sensor_choose": (ELECTSMART_SENSOR_ATTR, None),
}
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.thermostat_bus.add_listener(self)
self.endpoint.device.thermostat_bus.listener_event(
"temperature_change",
"min_heat_setpoint_limit",
ELECTSMART_MIN_TEMPERATURE_VAL,
)
self.endpoint.device.thermostat_bus.listener_event(
"temperature_change",
"max_heat_setpoint_limit",
ELECTSMART_MAX_TEMPERATURE_VAL,
)
def map_attribute(self, attribute, value):
"""Map standardized attribute value to dict of manufacturer values."""
if attribute in self.DIRECT_MAPPING_ATTRS:
return {
self.DIRECT_MAPPING_ATTRS[attribute][0]: (
value
if self.DIRECT_MAPPING_ATTRS[attribute][1] is None
else self.DIRECT_MAPPING_ATTRS[attribute][1](value)
)
}
elif attribute == "operation_preset":
if value == 1:
return {ELECTSMART_MODE_ATTR: 1}
if value == 2:
return {ELECTSMART_MODE_ATTR: 0}
elif attribute in ("programing_oper_mode", "occupancy"):
if attribute == "occupancy":
occupancy = value
oper_mode = self._attr_cache.get(
self.attributes_by_name["programing_oper_mode"].id,
self.ProgrammingOperationMode.Simple,
)
else:
occupancy = self._attr_cache.get(
self.attributes_by_name["occupancy"].id, self.Occupancy.Occupied
)
oper_mode = value
if occupancy == self.Occupancy.Occupied:
if oper_mode == self.ProgrammingOperationMode.Schedule_programming_mode:
return {ELECTSMART_MODE_ATTR: 1}
if oper_mode == self.ProgrammingOperationMode.Simple:
return {ELECTSMART_MODE_ATTR: 0}
self.error("Unsupported value for ProgrammingOperationMode")
else:
self.error("Unsupported value for Occupancy")
elif attribute == "system_mode":
if value == self.SystemMode.Off:
mode = 0
else:
mode = 1
return {ELECTSMART_SYSTEM_MODE_ATTR: mode}
def mode_change(self, value):
"""Preset Mode change."""
if value == 0:
operation_preset = self.Preset.Manual
prog_mode = self.ProgrammingOperationMode.Simple
else:
operation_preset = self.Preset.Schedule
prog_mode = self.ProgrammingOperationMode.Schedule_programming_mode
self._update_attribute(self.attributes_by_name["programing_oper_mode"].id, prog_mode)
self._update_attribute(self.attributes_by_name["occupancy"].id, self.Occupancy.Occupied)
self._update_attribute(self.attributes_by_name["operation_preset"].id, operation_preset)
def enabled_change(self, value):
"""System mode change."""
if value == 0:
mode = self.SystemMode.Off
else:
mode = self.SystemMode.Heat
self._update_attribute(self.attributes_by_name["system_mode"].id, mode)
class ElectsmartUserInterface(TuyaUserInterfaceCluster):
"""HVAC User interface cluster for tuya electric heating thermostats."""
_CHILD_LOCK_ATTR = ELECTSMART_CHILD_LOCK_ATTR
class ElectsmartChildLock(CustomTuyaOnOff):
"""On/Off cluster for the child lock function of the electric heating thermostats."""
def child_lock_change(self, value):
"""Child lock change."""
self._update_attribute(self.attributes_by_name["on_off"].id, value)
def map_attribute(self, attribute, value):
"""Map standardized attribute value to dict of manufacturer values."""
if attribute == "on_off":
return {ELECTSMART_CHILD_LOCK_ATTR: value}
class ElectsmartTempCalibration(LocalDataCluster, AnalogOutput):
"""Analog output for Temp Calibration."""
def __init__(self, *args, **kwargs):
"""Init."""
super().__init__(*args, **kwargs)
self.endpoint.device.ElectsmartTempCalibration_bus.add_listener(self)
self._update_attribute(
self.attributes_by_name["description"].id, "Temperature Calibration"
)
self._update_attribute(self.attributes_by_name["max_present_value"].id, 9)
self._update_attribute(self.attributes_by_name["min_present_value"].id, -9)
self._update_attribute(self.attributes_by_name["resolution"].id, 1)
self._update_attribute(self.attributes_by_name["application_type"].id, 13 << 16)
self._update_attribute(self.attributes_by_name["engineering_units"].id, 62)
def set_value(self, value):
"""Set value."""
self._update_attribute(self.attributes_by_name["present_value"].id, value)
def get_value(self):
"""Get value."""
return self._attr_cache.get(self.attributes_by_name["present_value"].id)
async def write_attributes(self, attributes, manufacturer=None):
"""Override the default Cluster write_attributes."""
for attrid, value in attributes.items():
if isinstance(attrid, str):
attrid = self.attributes_by_name[attrid].id
if attrid not in self.attributes:
self.error("%d is not a valid attribute id", attrid)
continue
self._update_attribute(attrid, value)
await ElectsmartManufClusterSelf[
self.endpoint.device.ieee
].endpoint.tuya_manufacturer.write_attributes(
{ELECTSMART_TEMP_CALIBRATION_ATTR: value},
manufacturer=None,
)
return ([foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)],)
class Electsmart(EnchantedDevice, TuyaThermostat):
"""Tuya thermostat for electsmart ES-111Z Electric floor heating."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.thermostat_onoff_bus = Bus()
self.ElectsmartTempCalibration_bus = Bus()
super().__init__(*args, **kwargs)
signature = {
MODELS_INFO: [
("_TZE200_edl8pz1k", "TS0601"),
("_TZE204_edl8pz1k", "TS0601"),
],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaManufClusterAttributes.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
242:{
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
ElectsmartManufCluster,
ElectsmartThermostat,
ElectsmartUserInterface,
TuyaPowerConfigurationCluster,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.CONSUMPTION_AWARENESS_DEVICE,
INPUT_CLUSTERS: [ElectsmartTempCalibration],
OUTPUT_CLUSTERS: [],
},
242: {
PROFILE_ID: 41440,
DEVICE_TYPE: 97,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
}
}
Далее рассмотрим что-же у них "внутри".
Внутри они сделаны близнецами-братьями. Различия только в прошивках "безымянного" микроконтроллера и RF-интерфейса ZT3L (Tuya module).
Приведу фотоснимок силовой платы Electsmart EST-111Z:
Контактная колодка j5 на силовой плате, распиновка 9контактов:
1 - +12v
2 - минус общий
3 - нога термодатчика низ
4 - на обвязку реле (ножка R7 резистора в базе транзистора ключевого режима)
5 - на обвязку реле (к не паянному R9 резистору)
6 - торчит в воздухе\не соединен
7 - нога термодатчика верх
8 - 485- не паяный разьем J4
9 - 485+ не паяный разьем J4
Полная принципиальная схема ниже.
Плата представляет собой:
- место для запайки двух узких мини-реле с маркировкой HF46F 12-HS1. Одно из которых запаяно а второе - нет. очевидно это задел под работу с охлаждением в "соседних моделях".
- Конвертор питания из сетевых 220 в 12 с развязкой.
Реле на 12вольт расчитаны. Поэтому ими управляют через транзисторный ключ с 5в от микроконтроллера с другой платы. Далее на плате распаян преобразователь сетевого напряжения с гальванической развязкой и пропилами необходимыми в плате (правда без искрового разрядника и без фильтрации ;). AC/DC SMPS представляет из себя классический flyback с aux обмоткой с которой снимается напряжение для коррекции "основной" обмотки. Собран он на базе мс фирмы On-Bright OB25133JB. Даташит легко находится в поиске. Здесь приведу референсную схему из даташита.
В схеме которая на плате отсутствуют некоторые элементы. например: фильтр индуктивность на входе. Немецкое качество-же :)
Так-же примечателен момент с JUMP1 перемычкой. дело в том что там напаяно толстым слоем олово и промежуток без дорожки и припоя расстоянием меньше 0.5мм что довольно чревато если посмотреть что идет соприкосновение "сухого контакта" реле ног с сетевым напряжением.
В моем экземпляре измеренное напряжение 12.4в. слишком задрано имхо и надо регулировать резисторы в цепи FB у микросхемы.
Теперь настал черед второй платы. основной. так она выглядит с 2мя микроконтроллерами у Electsmart EST-111Z
По своей сути плата:
- содержит 105 светодиодов соединенных, похоже, в матрицу. Ногами светодиоды через гасящие токо-ограничительные резисторы зацепленны за ножки микроконтроллера напрямую.
- Формирователи вторичного напряжения: из 12в в +3.3в (собран на базе ac/dc конвертора который не удалось идентифицировать. маркировка SOT32-6 GS2R . по распиновке очень похож на какой-то аналог TI LV2862)
- Формирователь вторичного напряжения: из 12в в +5в состоящий из линейного стабилизатора AMS1117 в редакции с произвольным заданием напряжения на выходе (ADJ). Напряжение выхода как-раз задано 4.7В а не 5. Возможно поэтому и применен был стаб не из стандартной линейки на 5В. Еще у ADJ регуляторов выше режекция помех чем у стандартных. Схема применения похожа на референс-дизайн кроме отсутствия диода шоттки на выходе. Место под него разведено но он физически не запаян.
- Центральный микроконтроллер с тщательно снесенной маркировкой. Выше я приводил возможные варианты этого MCU.
- микроконтроллер-модуль (готовый) для связи со внешним миром. он не во всех моделях присутствует. в данной это ZigBee модуль Tuya экосистемы под маркировкой ZT3L. (внутри это TeLink TLSR8258F1KAT32)
- Сенсорные кнопки для управления всем девайсом
- NTC-термистор для измерения внешней температуры воздуха в месте установки девайса
Общаются между собой безымянный MCU (со спиленой по-немецки маркировкой) и RF mcu через обычный uart. Судя по распиновке ног и информации представленной в верхней части данной статьи - цоколевка микроконтроллера следующая:
Примененный алгоритм регуляции температуры - гистерезисного принципа. Поэтому может довольно быстро механическим перещелкиванием убить реле если скажем температуру быстро будет набирать очень и быстро достаточно остывать. Настройку гистерезиса можно поменять в пределах +1...+5 изнутри меню настроек (в инструкции про это некорректно написано). Модуль ZT3L от Tuya, похоже, подразумевалось быстрым взмахом руки можно менять на любые интерфейсы взаимодействия со внешним миром не трогая прошивки и сам основной микроконтроллер. в моей модели описываемой он в корпусе LQFP64.
Впрочем, судя по фотографии брата-близнеца (в начале заметки) без модуля RF - это какой-то более старший микроконтроллер производителя CACHIP.
90% принципиальной схемы (светодиоды матрица коммутации не вся отрисована, сенсорные кнопки тоже) ниже.
Есть незапаянные светодиоды, как на фронтальной стороне, так и со стороны MCUшек (эти очевидно для какого-то эффекта подсветки каймы в каких-то моделях). Но среди запаяных светодиодов есть подсветка иконок отображающих текущее состояние погоды за окном, а так-же влажности rh/%. Очевидно что софтварная компонента должна уметь получать некие данные из внешних источников и отображать это. В переведенный на русский язык инструкции ничего про это не сказано впрочем.
Перейду к софтварной компоненте данного экземпляра:
Первое наблюдение это то как общается модуль по воздуху в сети Zigbee.
Отметил безумнейший DDOS сети ZigBee от терморегулятора. Если в сети будет больше одного - они запросто положат всю сеть на лопатки вместе с координатором и ZHA-интеграцией (проверено!).
Очевидно что "немецкое качество" тут столкнулось с суровой китайской реальностью которую можно описать коротко так:
Тяжело себе представляется картина когда гистерезисный принцип регуляции требовал бы по 4 отчета в секунду о состоянии "нагревает\ не нагревает". А учитывая что железка эта домашнего сегмента - то это просто "красивый" показометр и отсылать такие сообщения имеет смысл никак не раньше чем через минуту. Бомбардировать домашние сети этим явно не стоит. Или как минимум вытащить в какой-то атрибут конфигурации: как часто.
В термостате существует функция настройки работы с термодатчиком NTC-типа:
OUT = работа по внешнему. в 0x1802 tuya будет лежать значение внешнего термодатчика, в 0x6602 - оно же.
IN = работа по внутреннему. при этом в 0x1802 tuya будет лежать значение внутреннег термодатчика, в 0x6602 - будут нули
AL = работа о обеим термодатчикам. точно не понятна логика но в 0x1802 будет лежать значение внутреннего, а в 0x6602 - значние внешнего
Далее немножко про то какие команды удалось отловить в эфире при включении уже слинкованного термостата.
zcl command 0x02 = Device Status Query / Report
tuya data points:
0x24 | enum. статус нагревателя (вкл = 00 в последнем байте, выкл = 01 в последний байт) |
0x1a | int32. минимально задаваемая температура (5 ячейка настроек самого термостата) умноженная на 10 (дробь чтоб получать видимо). 5 градусов будет = 50 число |
0x1b | int32. компенсация температуры термодатчика (1 ячейка настроек самого термостата) без умножения |
0x01 | bool. on/off всего термостата. при 0 - выключен. при 1 - включен. |
0x02 | enum. режим работы термостата. при ручном режиме приходит 01. при расписании (часики пиктограмма) приходит ХХ |
0x10 | int32. температура setpoint (до которой греть) умноженная на 10 (дробь чтоб получать видимо). 13 градуссов приходит как число 130. |
0x14 | int32. минимально задаваемая пользователем температура. (5 ячейка настроек самого термостата) умноженная на 10 (дробь чтоб получать видимо). |
0x12 | int32. максимально задаваемая пользователем температура. (6 ячейка настроек самого термостата) умноженная на 10 (дробь чтоб получать видимо). |
0x2b | enum. тип используемого датчика (00 = IN, 01 = AL , 02 = OU) |
0x68 | int32. настройка защиты от высоких температур (9 ячейка настроек самого термостата) умноженная на 10 (дробь чтоб получать видимо). 45 градусов будет = 450 число |
0x66 | int32. текущая температура со внешнего термодатчика умноженная на 10. доступна только в режимах AL/OUT. в IN режиме работы она всегда 0 |
0x28 | bool. keypad_lock. 0 разлочен , 1 залочен. |
0x18 | int32. текущая температура с термодатчика умноженная на 10 (дробь чтоб получить видимо). |
0x65 | raw. относится к расписанию. 17 байт данных "07060000c80b1e00c80d1e00c8111e00c8". |
0x6e | raw. относится к расписанию. 17 байт данных "01060000c80b1e00c80d1e00c8111e00c8". возможно значение программирования шедуля расписания нагрева |
0x6d | raw. относится к расписанию. 17 байт данных "02060000c80b1e00c80d1e00c8111e00c8". |
0x6c | raw. относится к расписанию. 17 байт данных "03060000c80b1e00c80d1e00c8111e00c8". |
0x6b | raw. относится к расписанию. 17 байт данных "04060000c80b1e00c80d1e00c8111e00c8". |
0x6a | raw. относится к расписанию. 17 байт данных "05060000c80b1e00c80d1e00c8111e00c8". |
0x69 | raw. относится к расписанию. 17 байт данных "06060000c80b1e00c80d1e00c8111e00c8". |
0x6f | int32. ?непонятно что? |
при смене через настройки значения настроек 5ячейки, в сетку кидаются два сообщения с DPid = 0x1a и 0x14. их содержимое одно и тоже число.
остается непонятным - почему так.
Это далеко не полный список команд поскольку они брались в момент когда устройства уже слинкованы с сетью. При включении и установке соединения впервые с сетью - параметров должно быть гораздо больше.
Далее список Tuya Data Points который центральный MCU кидает на вход UART ZT3L модуля в момент когда он стыкуется с сетью. они получены из python-снифера выше зацепленого за RS232 порт и ножку RX-линии у модуля ZT3L:
DPid='0x01' function=0 len=1 payload=b'00' type=<DPType.BOOLEAN: 1>
DPid='0x02' function=0 len=1 payload=b'01' type=<DPType.ENUM: 4>
DPid='0x10' function=0 len=4 payload=b'000000dc' type=<DPType.INTEGER: 2>
DPid='0x12' function=0 len=4 payload=b'0000015e' type=<DPType.INTEGER: 2>
DPid='0x13' function=0 len=4 payload=b'0000015e' type=<DPType.INTEGER: 2>
DPid='0x14' function=0 len=4 payload=b'00000032' type=<DPType.INTEGER: 2>
DPid='0x18' function=0 len=4 payload=b'000000eb' type=<DPType.INTEGER: 2>
DPid='0x1a' function=0 len=4 payload=b'00000032' type=<DPType.INTEGER: 2>
DPid='0x1b' function=0 len=4 payload=b'fffffffd' type=<DPType.INTEGER: 2>
DPid='0x24' function=0 len=1 payload=b'01' type=<DPType.ENUM: 4>
DPid='0x2b' function=0 len=1 payload=b'01' type=<DPType.ENUM: 4>
DPid='0x65' function=0 len=17 payload=b'07060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
DPid='0x66' function=0 len=4 payload=b'00000000' type=<DPType.INTEGER: 2>
DPid='0x67' function=0 len=4 payload=b'00000001' type=<DPType.INTEGER: 2>
DPid='0x68' function=0 len=4 payload=b'000001c2' type=<DPType.INTEGER: 2>
DPid='0x69' function=0 len=17 payload=b'06060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
DPid='0x6a' function=0 len=17 payload=b'05060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
DPid='0x6b' function=0 len=17 payload=b'04060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
DPid='0x6c' function=0 len=17 payload=b'03060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
DPid='0x6d' function=0 len=17 payload=b'02060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
DPid='0x6e' function=0 len=17 payload=b'01060000c80b1e00c80d1e00c8111e00c8' type=<DPType.RAW: 0>
Список получен при помощи моего самопального скрипта для заглядывания в трафик между микроконтроллерами
При включении платы (когда светятся все светодиодные сегменты) потребление тока от 12В источника питания 85мА.
при обычной работе (когда сеть найдена и дисплей наполовину светится) 9мА. Довольно экономно если учесть что ZT3L находится в режиме роутера и не может спать (роутит создающийся спам соседними братьями своими). И дисплей не весь гаснет а только часть. Цифры температур, режима и кнопок продолжают работать.
Судя по руководству для разработчиков устройств Tuya - идеология состоит в том что-бы неважно через какую среду всегда внутри шел свой собственный протокол.
Кстати вот тут довольно много почерпнул про них.
В сети мне удалось найти прекрасный проект который по всей видимости призван решить проблемы устройств этого класса и всех его клонов https://github.com/slacky1965/tuya_thermostat_zrd Он представляет из себя прошивку второго MCU (модуля связи с ZigBee-сетью). и он становится "барьером" не допускающим флуд сети. Естественно при этом устройство становится уже без Tuya экосистемы.
Другой исследователь данных термостатов который смог сдампить прошивку 1.0.1 из оказавшегося у него ElectSmart, расковырял ее дебаггером и любезно со мной поделился информацией о том что все эти термостаты являются репликами ODM/OEM BECA Thermostat. Так-же поделился прошивкой 4.0.1 для основного MCU (если читаеш этот опус, СПАСИБО ТЕБЕ ОГРОМНЕЙШЕЕ ЗА ВСЕ !!!), в которой проблема спама сети zigbee через модуль RF - устранена полностью. Термостат раз в минуту высылает данные по температуре. Но в этой версии прошивки присутствует баг - теперь он только целые числа температуры воспринимает. хотя на дисплее по-прежнему пишет с 0.5 градусом их при задании setpoint. После прошивки устройство становится с идентификатором TS0601 _TZE200_edl8pz1k. Что судя по поиску в сети соответствует термостатам Avato.
Моя конечная цель - получить устройство не DDOSящее сеть с управляющим элементом симмистором (SSR - электронное реле). Поскоьку для применения у меня ток порядка 2-3Ампер то вполне должно хватить нечто типа Crydom CX240D5. это узкое мини-реле расчитаное на токи до 5А которое будет установлено на место оригинального электромеханического реле.
Статья будет дополняться и в текущем ее виде служит местом накопления информации уже полученной по данному устройству.
There are no published comments.
New comment