From 5c832816f8f8c7eeea4775b130549d8218c08d25 Mon Sep 17 00:00:00 2001 From: genmllc Date: Tue, 23 Dec 2025 10:55:32 +0100 Subject: [PATCH 1/3] Add Thermor Malicio 3 65L (1962) --- custom_components/cozytouch/model.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/custom_components/cozytouch/model.py b/custom_components/cozytouch/model.py index aa9df2c..6f6fe61 100644 --- a/custom_components/cozytouch/model.py +++ b/custom_components/cozytouch/model.py @@ -407,6 +407,20 @@ def get_model_infos(modelId: int, zoneName: str | None = None): 4: HEATING_MODE_PROG, } + elif modelId == 1962: + modelInfos["name"] = "Thermor Malicio 3 65L" + modelInfos["type"] = CozytouchDeviceType.WATER_HEATER + modelInfos["HVACModes"] = { + 0: HVACMode.OFF, + 4: HVACMode.HEAT, + } + + modelInfos["HeatingModes"] = { + 0: HEATING_MODE_MANUAL, + 3: HEATING_MODE_ECO_PLUS, + 4: HEATING_MODE_PROG, + } + elif modelId == 1966: modelInfos["name"] = "Thermor Malicio 3 120L" modelInfos["type"] = CozytouchDeviceType.WATER_HEATER From f72f20116fc85c42a9bc501a179ac0518216e558 Mon Sep 17 00:00:00 2001 From: genmllc Date: Tue, 23 Dec 2025 10:58:33 +0100 Subject: [PATCH 2/3] Add new capabilities based on retro-engineered Cozytouch Android APK --- custom_components/cozytouch/capability.py | 193 +++++++++++++++++- .../cozytouch/translations/en.json | 19 ++ .../cozytouch/translations/fr.json | 19 ++ 3 files changed, 230 insertions(+), 1 deletion(-) diff --git a/custom_components/cozytouch/capability.py b/custom_components/cozytouch/capability.py index 16c1f4e..0d3d0ca 100644 --- a/custom_components/cozytouch/capability.py +++ b/custom_components/cozytouch/capability.py @@ -175,12 +175,14 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["category"] = "sensor" elif capabilityId == 86: + # DHW_OPERATING_STATUS_CAPABILITY capability["name"] = "domestic_hot_water" capability["type"] = "switch" capability["category"] = "sensor" capability["icon"] = "mdi:faucet" elif capabilityId == 87: + # DHW_CURRENT_MODE capability["name"] = "heating_mode" capability["type"] = "select" capability["category"] = "sensor" @@ -204,6 +206,7 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["name"] = "resistance" capability["icon"] = "mdi:radiator" else: + # DHW_HEATING_STATUS capability["name"] = "dhw_pump" capability["icon"] = "mdi:faucet" @@ -346,6 +349,7 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["step"] = 0.5 elif capabilityId == 165: + # DHW_BOOST_ON capability["name"] = "boost_mode" capability["type"] = "switch" capability["category"] = "sensor" @@ -355,6 +359,24 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["value_off"] = "false" capability["value_on"] = "true" + elif capabilityId == 168: + # DHW_AVAILABLE_MODES_CAPABILITIES + # Based on DHWMode + # Bit Mask + # 1 MANUAL + # 4 AUTO + # 8 PROG + # 256 BOOST + # 512 BOOST_SCHEDULE + # 1024 ABSENCE + # 2048 ABSENCE_SCHEDULE + # 4096 ANTILEGIONELLA + # 8192 SMART_GRID + # Ex : 7941 => MANUAL, AUTO, BOOST, BOOST_SCHEDULE, ABSENCE, ABSENCE_SCHEDULE, ANTILEGIONELLA + capability["name"] = "available_modes" + capability["type"] = "string" + capability["category"] = "diag" + elif capabilityId == 169: capability["name"] = "radio_signal" capability["type"] = "percentage" @@ -463,7 +485,18 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["name"] = "prog_14_z2" capability["type"] = "prog" capability["category"] = "diag" - + + elif capabilityId == 218: + # WIFI_CONNECTED + # Based on WifiState + # UNKNOWN: 0 + # BLINKING: 1 + # NOT_BLINKING: 2 + capability["name"] = "wifi_connected" + capability["type"] = "string" + capability["category"] = "diag" + capability["icon"] = "mdi:wifi" + elif capabilityId == 219: capability["name"] = "wifi_ssid" capability["type"] = "string" @@ -471,6 +504,8 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["icon"] = "mdi:wifi" elif capabilityId in (222, 226): + # 222 : ABSENCE_HOME_DATE + # 226 : ABSENCE_DHW_DATE capability["name"] = "away_mode" capability["name_0"] = "away_mode_start" capability["name_1"] = "away_mode_stop" @@ -484,7 +519,56 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s else: capability["capabilityDuplicate"] = 222 + elif capabilityId == 223: + # DHW_AVAILABLE_HEATING_TYPE + # Based on DHWHeatingType + # Enum Mask (id) Value (String) + # HEAT_AVAILABLE 1 "0" + # HEAT_SCHEDULE 2 "1" + # HEAT_OFF_PEAK 4 "2" + # EAT_SELF_CONSUMPTION 8 "-1" + # UNKNOWN -1 "-1" + capability["name"] = "available_heating_type" + capability["type"] = "string" + capability["category"] = "diag" + + elif capabilityId == 224: + # DHW_ESTIMATION_SUPPORT + capability["name"] = "estimation_support" + capability["type"] = "string" + capability["category"] = "diag" + + elif capabilityId == 228: + # ABSENCE_DHW_STATE + # Based on GacomaAbsenceState + # DISABLED: "0" + # ENABLED: "1" + # SCHEDULED: "2" + # UNKNOWN: "-1" + capability["name"] = "absence_state" + capability["type"] = "string" + capability["category"] = "diag" + + elif capabilityId == 228: + # ABSENCE_DHW_TEMPERATURE + capability["name"] = "absence_temperature" + capability["type"] = "temperature" + capability["category"] = "diag" + + elif capabilityId == 230: + # DHW_CURRENT_HEATING_TYPE + # Based on DHWHeatingType + # 1 -> HEAT_AVAILABLE + # 2 -> HEAT_SCHEDULE + # 4 -> HEAT_OFF_PEAK + # 8 -> HEAT_SELF_CONSUMPTION + # -1 -> UNKNOWN + capability["name"] = "current_heating_type" + capability["type"] = "string" + capability["category"] = "diag" + elif capabilityId == 231: + # DHW_CURRENT_MANUAL_TARGET_SET_BY_USER capability["name"] = "target_temperature" capability["type"] = "temperature_adjustment_number" capability["category"] = "sensor" @@ -492,6 +576,7 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["highestValueCapabilityId"] = 105304 elif capabilityId == 232: + # DHW_BOOST_MINUTES capability["name"] = "boost_total_time" capability["type"] = "time" capability["category"] = "diagnostic" @@ -537,6 +622,18 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["name"] = "prog_07" capability["type"] = "progtime" capability["category"] = "diag" + + elif capabilityId == 252: + # DHW_MAX_USER_TARGET + capability["name"] = "max_user_target" + capability["type"] = "temperature" + capability["category"] = "diag" + + elif capabilityId == 253: + # DHW_MIN_USER_TARGET + capability["name"] = "min_user_target" + capability["type"] = "temperature" + capability["category"] = "diag" elif capabilityId == 258: capability["name"] = "tank_capacity" @@ -582,15 +679,40 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["icon"] = "mdi:water-thermometer" elif capabilityId == 271: + # DHW_STATE_OF_CHARGE capability["name"] = "hot_water_available" capability["type"] = "percentage" capability["category"] = "sensor" + + elif capabilityId == 280: + # DHW_COLD_WATER_TEMPERATURE + capability["name"] = "cold_water_temperature" + capability["type"] = "temperature" + capability["category"] = "diag" elif capabilityId == 283: capability["name"] = "off_peak_hours" capability["type"] = "binary" capability["category"] = "sensor" capability["icon"] = "mdi:clock-outline" + + elif capabilityId == 290: + # ERROR_CODE_DHW + capability["name"] = "error_code" + capability["type"] = "string" + capability["category"] = "diag" + + elif capabilityId == 307: + # DHW_MIN_HEATING_DURATION_PERIOD_FOR_ONE_DAY - in minutes + capability["name"] = "min_heating_duration_period_for_one_day" + capability["type"] = "int" + capability["category"] = "diag" + + elif capabilityId == 312: + # DHW_CURRENT_CONTROL_TARGET + capability["name"] = "current_control_target" + capability["type"] = "temperature" + capability["category"] = "diag" elif capabilityId == 315: capability["name"] = "timezone" @@ -603,7 +725,31 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["type"] = "string" capability["category"] = "diag" capability["icon"] = "mdi:tag" + + elif capabilityId == 329: + # DHW_MIN_NUMBER_PROGRAMMING_RANGE_PER_DAY + capability["name"] = "min_number_programming_range_per_day" + capability["type"] = "int" + capability["category"] = "diag" + + elif capabilityId == 331: + # DHW_MAX_DURATION_PROG_RANGE + capability["name"] = "max_duration_prog_range" + capability["type"] = "int" + capability["category"] = "diag" + elif capabilityId == 332: + # DHW_MIN_DURATION_PROG_RANGE + capability["name"] = "min_duration_prog_range" + capability["type"] = "int" + capability["category"] = "diag" + + elif capabilityId == 333: + # DHW_MAX_HEATING_DURATION_PERIOD_FOR_ONE_DAY + capability["name"] = "max_heating_duration_period_for_one_day" + capability["type"] = "int" + capability["category"] = "diag" + elif capabilityId == 335: capability["name"] = "serial_number" capability["type"] = "string" @@ -740,8 +886,52 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["lowest_value"] = 5 capability["highest_value"] = 60 capability["step"] = 5 + + elif capabilityId == 105011: + # DHW_SUPPORTED_MODES_CAPABILITIES + # Based on DHWMode + # Bit Mask + # 1 MANUAL + # 4 AUTO + # 8 PROG + # 256 BOOST + # 512 BOOST_SCHEDULE + # 1024 ABSENCE + # 2048 ABSENCE_SCHEDULE + # 4096 ANTILEGIONELLA + # 8192 SMART_GRID + # Ex : 16141 => MANUAL, AUTO, PROG, BOOST, BOOST_SCHEDULE, ABSENCE, ABSENCE_SCHEDULE, ANTILEGIONELLA, SMART_GRID + capability["name"] = "supported_modes" + capability["type"] = "int" + capability["category"] = "diag" + + elif capabilityId == 105012: + # DHW_SUPPORTED_HEATING_TYPE + # Based on DHWHeatingType + # Bit Mask + # 1 -> HEAT_AVAILABLE + # 2 -> HEAT_SCHEDULE + # 4 -> HEAT_OFF_PEAK + # 8 -> HEAT_SELF_CONSUMPTION + # -1 -> UNKNOWN + capability["name"] = "supported_heating_type" + capability["type"] = "int" + capability["category"] = "diag" + + elif capabilityId == 105122: + # DHW_BOOST_END_TIMESTAMP + capability["name"] = "boost_end_timestamp" + capability["type"] = "int" + capability["category"] = "diag" + + elif capabilityId == 105300: + # DHW_WATER_LIMIT + capability["name"] = "water_limit" + capability["type"] = "number" + capability["category"] = "diag" elif capabilityId == 105906: + # DHW_V40_APPLIED_SETPOINT capability["name"] = "Target 105906" capability["type"] = "temperature_percent_adjustment_number" capability["category"] = "sensor" @@ -749,6 +939,7 @@ def get_capability_infos(modelInfos: dict, capabilityId: int, capabilityValue: s capability["temperatureMax"] = 65.0 elif capabilityId == 105907: + # DHW_V40_MANUALLY_FILLED_BY_USER capability["name"] = "Target 105907" capability["type"] = "temperature_percent_adjustment_number" capability["category"] = "sensor" diff --git a/custom_components/cozytouch/translations/en.json b/custom_components/cozytouch/translations/en.json index be2eca3..77491d2 100644 --- a/custom_components/cozytouch/translations/en.json +++ b/custom_components/cozytouch/translations/en.json @@ -164,22 +164,31 @@ } }, "sensor": { + "absence_state": { "name": "Absence state" }, "air_conditioner": { "name": "Air Conditioner" }, + "available_heating_type": { "name": "Available heating types" }, + "available_modes": { "name": "Available modes" }, "away_mode": { "name": "Away Mode" }, "away_mode_start": { "name": "Away Mode Start" }, "away_mode_stop": { "name": "Away Mode Stop" }, "boiler_water_temperature": { "name": "Boiler Water Temperature" }, + "boost_end_timestamp": { "name": "Boost End timestamp" }, "boost_mode": { "name": "Boost Mode" }, "boost_total_time": { "name": "Boost Total Time" }, "boost_remaining_time": { "name": "Boost Remaining Time" }, "central_heating": { "name": "Central Heating" }, "ch_power_consumption": { "name": "CH Power consumption" }, + "cold_water_temperature": { "name": "Cold water temperature" }, "condenser_temperature": { "name": "Condenser Temperature" }, + "current_control_target": { "name": "Current Control Target"}, + "current_heating_type": { "name": "Current Heating Type"}, "dhw_power_consumption": { "name": "DHW Power consumption" }, "dhw_pump": { "name": "DHW Pump" }, "dhw_temperature": { "name": "DHW Temperature" }, "domestic_hot_water": { "name": "Domestic Hot Water" }, "eco_mode": { "name": "Eco Mode" }, + "error_code": { "name": "Error code"}, + "estimation_support": { "name": "Estimation Support"}, "exhaust_temperature": { "name": "Exhaust Temperature" }, "flame": { "name": "Flame" }, "heat": { "name": "Heat" }, @@ -188,6 +197,13 @@ "hot_water_available": { "name": "Hot Water Available" }, "interface_fw": { "name": "Interface FW" }, "temperature_setpoint": { "name": "Temperature Setpoint" }, + "max_duration_prog_range": { "name": "Maximum duration of the Prog Range (min)"}, + "max_heating_duration_period_for_one_day": { "name": "Maximum heating duration period for one day (min)"}, + "max_user_target": { "name": "Maximum target temperature set by the user"}, + "min_duration_prog_range": { "name": "Minimum duration of the Prog Range"}, + "min_heating_duration_period_for_one_day": { "name": "Minimum heating duration period for one day (min)"}, + "min_number_programming_range_per_day": { "name": "Minimum number of programming range per day"}, + "min_user_target": { "name": "Minimum target temperature set by the user"}, "model_name": { "name": "Model" }, "number_of_hours_burner": { "name": "Number of hours burner" }, "number_of_hours_ch_pump": { "name": "Number of hours CH pump" }, @@ -244,6 +260,8 @@ "radio_signal": { "name": "Radio Signal" }, "resistance": { "name": "Resistance" }, "serial_number": { "name": "Serial Number" }, + "supported_heating_type": { "name": "Supported Heating Type"}, + "supported_modes": { "name": "Supported Modes"}, "swing_mode": { "name": "Swing Mode" }, "tank_bottom_temperature": { "name": "Tank Bottom Temperature" }, "tank_capacity": { "name": "Tank Capacity" }, @@ -263,6 +281,7 @@ "version": { "name": "Version" }, "water_consumption": { "name": "Water Consumption"}, "water_pressure": { "name": "Water Pressure" }, + "wifi_connected": { "name": "Wifi Connected"}, "wifi_signal": { "name": "Wifi Signal" }, "wifi_ssid": { "name": "Wifi SSID" }, "zone_1": { "name": "Zone 1" }, diff --git a/custom_components/cozytouch/translations/fr.json b/custom_components/cozytouch/translations/fr.json index bfa1e76..23aab33 100644 --- a/custom_components/cozytouch/translations/fr.json +++ b/custom_components/cozytouch/translations/fr.json @@ -164,22 +164,31 @@ } }, "sensor": { + "absence_state": { "name": "Status Absence" }, "air_conditioner": { "name": "Climatisation" }, + "available_heating_type": { "name": "Type de chauffage disponibles" }, + "available_modes": { "name": "Modes disponibles" }, "away_mode": { "name": "Absence" }, "away_mode_start": { "name": "Absence Début" }, "away_mode_stop": { "name": "Absence Fin" }, "boiler_water_temperature": { "name": "Température Chaudière" }, + "boost_end_timestamp": { "name": "Timestamp de fin de Boost" }, "boost_mode": { "name": "Mode Boost" }, "boost_total_time": { "name": "Durée Boost Totale" }, "boost_remaining_time": { "name": "Durée Boost Restante" }, "central_heating": { "name": "Chauffage" }, "ch_power_consumption": { "name": "Consommation CH" }, + "cold_water_temperature": { "name": "Temperature de l'eau froide" }, "condenser_temperature": { "name": "Temperature Condensateur" }, + "current_control_target": { "name": "Consigne de régulation actuelle"}, + "current_heating_type": { "name": "Type de chauffage actuel"}, "dhw_power_consumption": { "name": "Consommation ECS" }, "dhw_pump": { "name": "Pompe ECS" }, "dhw_temperature": { "name": "Température ECS" }, "domestic_hot_water": { "name": "Eau Chaude" }, "eco_mode": { "name": "Mode Eco" }, + "error_code": { "name": "Code d'erreur"}, + "estimation_support": { "name": "Prise en charge de l’estimation"}, "exhaust_temperature": { "name": "Température Échappement" }, "flame": { "name": "Flamme" }, "heat": { "name": "Chauffage" }, @@ -188,6 +197,13 @@ "hot_water_available": { "name": "Hot Water Available" }, "interface_fw": { "name": "Interface FW" }, "temperature_setpoint": { "name": "Température Consigne" }, + "max_duration_prog_range": { "name": "Durée maximale de la plage de programmation (min)"}, + "max_heating_duration_period_for_one_day": { "name": "Durée maximale de chauffage sur une journée (min)"}, + "max_user_target": { "name": "Température de consigne maximale définie par l’utilisateur"}, + "min_duration_prog_range": { "name": "Durée minimale de la plage de programmation (min)"}, + "min_heating_duration_period_for_one_day": { "name": "Durée minimale de chauffage sur une journée (min)"}, + "min_number_programming_range_per_day": { "name": "Nombre minimal de plages de programmation par jour"}, + "min_user_target": { "name": "Température de consigne minimale définie par l’utilisateur"}, "model_name": { "name": "Modèle" }, "number_of_hours_burner": { "name": "Nb heures brûleur" }, "number_of_hours_ch_pump": { "name": "Nb heures pompe CH" }, @@ -244,6 +260,8 @@ "radio_signal": { "name": "Signal Radio" }, "resistance": { "name": "Résistance" }, "serial_number": { "name": "Numéro de Série" }, + "supported_heating_type": { "name": "Type de chauffage supportés"}, + "supported_modes": { "name": "Modes supportés"}, "swing_mode": { "name": "Mode Oscillation" }, "tank_bottom_temperature": { "name": "Température Bas Réservoir" }, "tank_capacity": { "name": "Capacité Réservoir" }, @@ -263,6 +281,7 @@ "version": { "name": "Version" }, "water_consumption": { "name": "Consommation Eau"}, "water_pressure": { "name": "Pression Eau" }, + "wifi_connected": { "name": "Wifi connecté"}, "wifi_signal": { "name": "Signal Wifi" }, "wifi_ssid": { "name": "Wifi SSID" }, "zone_1": { "name": "Zone 1" }, From 64834645273a13d9c18c25727252b0d6fb32d0e2 Mon Sep 17 00:00:00 2001 From: genmllc <68810340+genmllc@users.noreply.github.com> Date: Tue, 23 Dec 2025 11:00:02 +0100 Subject: [PATCH 3/3] Add water heater model to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c9ef95a..d90b028 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This has been tested using on : - `Takao M3` air conditionning - `Kelud 1750W` towel rack - `Sauter Asama Connecté II Ventilo 1750W` towel rack + - `Thermor Malicio 3 65L` water heater A special mapping needs to be done for each model type, feel free to create an issue to help supporting your device.