Git Product home page Git Product logo

Comments (9)

sjorge avatar sjorge commented on May 12, 2024 1

I’ve done this manually in node-red and really struggled... I found a HeaterCooler service to map better than a ThermostatService for what most zigbee TRV’s expose.

It’s certainly doable thoigh.

from homebridge-z2m.

jhm47 avatar jhm47 commented on May 12, 2024

this is the API doc I meant

https://developers.homebridge.io/#/service/Thermostat

from homebridge-z2m.

itavero avatar itavero commented on May 12, 2024

I'm currently focusing on getting a new major version ready with several internal improvements (see #18).

I've had a brief look into thermostats in general. I think adding support for them should not be too difficult, but I have yet to look at the specifics.
Biggest challenge is probably testing it, as I don't have a Zigbee thermostat myself.

I'll keep this issue in the "backlog" and hopefully I'll be able to get to it in a couple of weeks.

from homebridge-z2m.

jhm47 avatar jhm47 commented on May 12, 2024

Cool

I got them so I can test if that helps :)

from homebridge-z2m.

itavero avatar itavero commented on May 12, 2024

@jhm47 Can you already post the following information?

  • Entry for this device from zigbee2mqtt/bridge/info/devices (you'll need a recent version of zigbee2mqtt for this)
  • Some MQTT messages of status updates from this device

This will help in writing some tests and writing the implementation, once I get to it.

from homebridge-z2m.

jhm47 avatar jhm47 commented on May 12, 2024

from homebridge-z2m.

itavero avatar itavero commented on May 12, 2024

@sjorge As more people are requesting support for TRVs and other climate devices, I wonder if you would be able to share how you've mapped your devices (which properties map to which characteristics).

If I have a look at devices.js, at first glance, I would think most of them can be mapped to a Thermostat. With the Heater Cooler I'm missing the setpoint (Target Temperature). Of course I can just add the characteristic, but it's not part of the "default" set of characteristics specified for the Heater Cooler.

from homebridge-z2m.

sjorge avatar sjorge commented on May 12, 2024

I'm using node-red, but the characteristics should be the same looks like I ended up switching back to Thermostat on the latest version of my subflow.

{
    "CurrentTemperature": true,
    "TargetTemperature": {
        "minValue": 8,
        "maxValue": 30,
        "minStep": 0.5
    },
    "CurrentHeatingCoolingState": true,
    "TargetHeatingCoolingState": {
        "validValues": [
            1
        ]
    }
}

Here is the function that does my mapping in node red,

/*
 * processor
 * ----------------------------------------------------
 * A ticker is used to trigger the main event loop.
 * The ticker sets the pace of handling the flow state.
 */
 
 /*
  * homekit mappings
  * ----------------------------------------------------
  * Active: 0=off, 1=heat, 2=cool, 3=auto
  * TargetHeaterCoolerState: 0=auto, 1=heat, 2=cool
  *
  * zigbee mappings (TUYA)
  * ----------------------------------------------------
  * system_mode: off, auto, manual, boost
  * child_lock (get): LOCKED, UNLOCKED
  * child_lock (set): LOCK, UNLOCK
  * auto_lock (set): AUTO, MANUAL
  * position: 0-100 (valve open %) [??? missing]
  *
  * zigbee mappings (eurotronic)
  * ----------------------------------------------------
  * system_mode: off, heat (boost), auto
  * eurotronic_host_flags.child_protection: true or false
  * pi_heating_demand: 0-100 (valve open %)
  *
  * zigbee mappings (ViCare)
  * ----------------------------------------------------
  * pi_heating_demand: 0-5 (0 closed, 1-5 ???) -- REMOVED
  *
  */

 
// variables
var state_base = `$parent.${env.get("MQTT_PREFIX")}/${env.get("MQTT_DEVICE")}`;
var sync_timeout = 5000;
var brand_supported = ["TUYA", "EUROTRONIC", "VICARE"];
var brand = (env.get("BRAND")||"EUROTRONIC").toUpperCase();
var low_battery = env.get("BATTERY_LOW_PCT")||0;
var ret = {
    "continue": {"reset": true},
    "mqtt": null,
    "homekit_thermostat": null,
    "homekit_battery": null,
};

// helpers functions
function stateGet(state_name, state_default=null, persistent=true) {
    return flow.get(`${state_base}/${state_name}`, (persistent ? "persistent" : null))||state_default;
}

function stateSet(state_name, state, persistent=true) {
    flow.set(`${state_base}/${state_name}`, state, (persistent ? "persistent" : null));
}

function stateGetAndSet(state_name, state, state_default=null, persistent=true) {
    var ret_state = stateGet(state_name, state_default, persistent);
    stateSet(state_name, state, persistent);
    return ret_state;
}

function homekitThermostatPublish(ret, prop, value) {
    // initialize ret.homekit_thermostat
    if (ret.homekit_thermostat === null) {
        ret.homekit_thermostat = {
            "topic": `${env.get("MQTT_PREFIX")}/${env.get("MQTT_DEVICE")}`,
            "payload": {},
        };
    }
    
    // update payload
    ret.homekit_thermostat.payload[prop] = value;
}

function homekitBatteryPublish(ret, prop, value) {
    // initialize ret.homekit_battery
    if (ret.homekit_battery === null) {
        ret.homekit_battery = {
            "topic": `${env.get("MQTT_PREFIX")}/${env.get("MQTT_DEVICE")}`,
            "payload": {},
        };
    }
    
    // update payload
    ret.homekit_battery.payload[prop] = value;
}

function mqttPublish(ret, prop, value) {
    // initialize ret.homekit
    if (ret.mqtt === null) {
        ret.mqtt = {
            "topic": `${env.get("MQTT_PREFIX")}/${env.get("MQTT_DEVICE")}/set`,
            "payload": {},
        };
    }
    
    // update payload
    ret.mqtt.payload[prop] = value;
}

function timestamp() {
    return Math.round((new Date()).getTime());
}

// main logic controller
if (!brand_supported.includes(brand)) {
    node.error(`Unsupported TRV brand ${brand}!`);
    return;
} else if (stateGet("sync", null, false) === null) {
    stateSet("queue_mqtt", stateGet("state", {}));
    stateSet("sync", (timestamp() - sync_timeout), false);
}

if (Object.keys(stateGet("queue_homekit", {})).length > 0) {
    // process homekit change queue
    var homekit = stateGetAndSet("queue_homekit", {}, {});
    // convert homekit to mqtt
    for (var hp in homekit) {
        var hv = homekit[hp];
        switch(hp) {
            case "Active":
                if (hv === 0) {
                    switch(brand) {
                        case "TUYA":
                        case "EUROTRONIC":
                            mqttPublish(ret, "system_mode", "off");
                            break;
                    }
                } else {
                    switch(brand) {
                        case "TUYA":
                            mqttPublish(ret, "system_mode", "manual");
                            break;
                        case "EUROTRONIC":
                            mqttPublish(ret, "system_mode", "auto");
                            break;
                    }
                }
                break;
            case "TargetTemperature":
            case "HeatingThresholdTemperature":
                switch(brand) {
                    case "TUYA":
                        mqttPublish(ret, "current_heating_setpoint", Math.max(Math.min(hv, 30), 5));
                        break;
                    case "EUROTRONIC":
                        mqttPublish(ret, "current_heating_setpoint", Math.max(Math.min(hv, 30), 10));
                        break;
                    case "VICARE":
                        mqttPublish(ret, "occupied_heating_setpoint", Math.max(Math.min(hv, 30), 8));
                        break;
                }
                break;
            case "LockPhysicalControls":
                switch(brand) {
                    case "TUYA":
                        mqttPublish(ret, "child_lock", ((hv == 1) ? "LOCK" : "UNLOCK"));
                        mqttPublish(ret, "auto_lock", ((hv == 1) ? "AUTO" : "MANUAL"));
                        break;
                    case "EUROTRONIC":
                        mqttPublish(ret, "eurotronic_host_flags", {"child_protection": ((hv == 1) ? true : false)});
                        break;
                }
                break;
            case "TemperatureDisplayUnits":
                switch(brand) {
                    case "VICARE":
                        homekitThermostatPublish(ret, "TemperatureDisplayUnits", 0);
                        break;
                }
                break;
        }
    }
    
    // update cache
    if (ret.mqtt !== null) {
        stateSet("sync", timestamp(), false);
        stateSet("state",
            Object.assign(stateGet("state", {}, true), ret.mqtt.payload));
    }
} else if (
    (Object.keys(stateGet("queue_mqtt", {})).length > 0) && 
    ((timestamp() - stateGet("sync", 0, false)) >= sync_timeout)
) {
    // process mqtt change queue
    var mqtt = stateGetAndSet("queue_mqtt", {}, {});
    
    if (!mqtt.hasOwnProperty("available") || mqtt.available) {
        // convert mqtt to homekit
        for (var mp in mqtt) {
            var mv = mqtt[mp];
            
            switch(mp) {
                case "available":
                    delete mqtt.available;
                    break;
                case "local_temperature":
                    homekitThermostatPublish(ret, "CurrentTemperature", mv);
                    // NOTE: TuYa and ViCare lack pi_heating_demand
                    if (["VICARE", "TUYA"].includes(brand)) {
                        var temp_target = 0.0;
                        var temp_actual = 0.0;
                        
                        if (mqtt.current_heating_setpoint) {
                            temp_target = parseFloat(mqtt.current_heating_setpoint);
                        } else if (stateGet("state", {}).current_heating_setpoint) {
                            temp_target = parseFloat(stateGet("state", {}).current_heating_setpoint);
                        }
                        
                        temp_actual = parseFloat(mv);
                        
                        switch(brand) {
                            case "VICARE":
                                homekitThermostatPublish(ret, "CurrentHeatingCoolingState", ((temp_actual >= temp_target)? 0 : 1));
                                break;
                            case "TUYA":
                                homekitThermostatPublish(ret, "CurrentHeaterCoolerState", ((temp_actual >= temp_target) ? 1 : 2));
                                break;
                        }
                    }
                    break;
                case "occupied_heating_setpoint":
                case "current_heating_setpoint":
                    switch(brand) {
                        case "TUYA":
                            homekitThermostatPublish(ret, "HeatingThresholdTemperature", Math.max(Math.min(mv, 30), 5));
                            break;
                        case "EUROTRONIC":
                            homekitThermostatPublish(ret, "HeatingThresholdTemperature", Math.max(Math.min(mv, 30), 10));
                            break;
                        case "VICARE":
                            homekitThermostatPublish(ret, "TargetTemperature", Math.max(Math.min(mv, 30), 8));
                            break;
                    }
                    break;
                case "system_mode":
                    switch(brand) {
                        case "VICARE":
                            // NOTE: not mapped for thermostat service
                            break;
                        default:
                            homekitThermostatPublish(ret, "Active", ((mv == "off") ? 0 : 1));
                            homekitThermostatPublish(ret, "TargetHeaterCoolerState", 1);
                            break;

                    }
                    break;
                case "pi_heating_demand":
                case "position":
                    switch(brand) {
                        case "VICARE":
                            homekitThermostatPublish(ret, "CurrentHeatingCoolingState", ((mv === 0) ? 0 : 1));
                            break;
                        default:
                            homekitThermostatPublish(ret, "CurrentHeaterCoolerState", ((mv === 0) ? 1 : 2));
                            break;

                    }
                    break;
                case "child_lock":
                    homekitThermostatPublish(ret, "LockPhysicalControls", ((mv == "LOCKED") ? 1 : 0));
                    break;
                case "eurotronic_host_flags":
                    homekitThermostatPublish(ret, "LockPhysicalControls", mv.child_protection ? 1 : 0);
                    break;
                case "battery":
                    homekitBatteryPublish(ret, "BatteryLevel", mv);
                    if (low_battery !== 0) {
                        homekitBatteryPublish(ret, "StatusLowBattery", (mv <= low_battery ? 1 : 0));
                    }
                    break;
                case "battery_low":
                    if (low_battery === 0) {
                        homekitBatteryPublish(ret, "StatusLowBattery", (mv ? 1 : 0));
                    }
                    break;
            }
        }
        
        // always mark 'ViCare' as active
        if (brand == 'VICARE') {
            homekitThermostatPublish(ret, "TargetHeatingCoolingState", 1);
        }

        // update cache
        stateSet("state",
            Object.assign(stateGet("state", {}), mqtt));
    } else {
        // mark dev offline
        switch(brand) {
            case "VICARE":
                homekitThermostatPublish(ret, "TargetHeatingCoolingState", "NO_RESPONSE");
                break;
            default:
                homekitThermostatPublish(ret, "Active", "NO_RESPONSE");
                break;
        }
    }
}

// publish messages
return [
    ret.continue,
    ret.mqtt,
    ret.homekit_thermostat,
    ret.homekit_battery,
];

Thermostats/TRV are one of my most hated things in zigbee2mqtt, half follow the zigbee mapping (IMHO, the correct way) other follow the hassio mapping of certain things like system_mode. It's a royal mess :(

from homebridge-z2m.

itavero avatar itavero commented on May 12, 2024

Opened up #40 to group all the individual request for thermostat/TRV support in one place, as these will most likely be handled by the same code.

Because of that I'm closing this issue. Updates/discussion should be done in this new issue.

from homebridge-z2m.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.