[External Converter]: OTH3600-GA-ZB from Ouellet
Link
https://www.ouellet.com/en-ca/products/thermostats-and-controls/oth3600-ga-zb.aspx
Database entry
{"id":15,"type":"Router","ieeeAddr":"0x500b9140000204af","nwkAddr":62611,"manufId":4508,"manufName":"Sinope Technologies\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000","powerSource":"Mains (single phase)","modelId":"OTH3600-GA-ZB\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000","epList":[1],"endpoints":{"1":{"profId":260,"epId":1,"devId":769,"inClusterList":[0,3,4,5,513,516,1026,1794,2820,2821,65281],"outClusterList":[10,65281,25],"clusters":{"65281":{"attributes":{"268":0,"277":0}},"genBasic":{"attributes":{"hwVersion":0}},"hvacThermostat":{"attributes":{"localTemp":2430,"occupiedHeatingSetpoint":700,"pIHeatingDemand":0,"ctrlSeqeOfOper":2,"systemMode":4}},"seMetering":{"attributes":{"currentSummDelivered":1596176}},"haElectricalMeasurement":{"attributes":{"1361":1595969,"rmsVoltage":240,"rmsCurrent":190,"apparentPower":456,"acCurrentMultiplier":1,"acCurrentDivisor":100}},"manuSpecificSinope":{"attributes":{"268":0,"277":0,"floorTemperature":2211,"roomTemperature":2289,"outdoorTempToDisplay":1000}},"msTemperatureMeasurement":{"attributes":{"tolerance":50,"measuredValue":2428}}},"binds":[{"cluster":0,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":3,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":513,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":516,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":2820,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":1794,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":4,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":1026,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1},{"cluster":65281,"type":"endpoint","deviceIeeeAddress":"0xe406bffffe21ad0c","endpointID":1}],"configuredReportings":[{"cluster":513,"attrId":8,"minRepIntval":0,"maxRepIntval":3600,"repChange":10},{"cluster":65281,"attrId":277,"minRepIntval":1,"maxRepIntval":3600,"repChange":1,"manufacturerCode":4508},{"cluster":65281,"attrId":268,"minRepIntval":1,"maxRepIntval":3600,"repChange":1,"manufacturerCode":4508},{"cluster":513,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":10},{"cluster":513,"attrId":18,"minRepIntval":0,"maxRepIntval":3600,"repChange":10},{"cluster":1026,"attrId":0,"minRepIntval":10,"maxRepIntval":3600,"repChange":100}],"meta":{}}},"appVersion":2,"stackVersion":2,"hwVersion":0,"dateCode":"20191021\u0000\u0000\u0000\u0000\u0000\u0000\u0000","swBuildId":"746\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000","zclVersion":2,"interviewCompleted":true,"interviewState":"SUCCESSFUL","meta":{"ctrlSeqeOfOper":{"1":2},"configured":"0.0.0"},"lastSeen":1789591403984}
Zigbee2MQTT version
2.14.1 (unknown)
External converter
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const {Zcl} = require('zigbee-herdsman');
const e = exposes.presets;
const ea = exposes.access;
const manuSinope = {
manufacturerCode: 0x119c,
};
/*
* ==========================================================================
* HELPERS
* ==========================================================================
*/
function getCached(endpoint, cluster, attribute) {
try {
return endpoint.getClusterAttributeValue(cluster, attribute);
} catch {
return undefined;
}
}
/*
* Electrical measurements for the Ouellet OTH3600-GA-ZB.
*
* Observed scaling:
*
* rmsVoltage = volts
* rmsCurrent / 100 = amperes
*
* Power:
*
* P = V × I
*
* When PI heating demand is 0%, current and power are forced to zero.
*/
function calculateElectrical(endpoint, newValues = {}) {
const result = {};
const voltage =
newValues.voltage !== undefined
? Number(newValues.voltage)
: Number(
getCached(
endpoint,
'haElectricalMeasurement',
'rmsVoltage',
),
);
const rawCurrent =
newValues.rawCurrent !== undefined
? Number(newValues.rawCurrent)
: Number(
getCached(
endpoint,
'haElectricalMeasurement',
'rmsCurrent',
),
);
const demand =
newValues.demand !== undefined
? Number(newValues.demand)
: Number(
getCached(
endpoint,
'hvacThermostat',
'pIHeatingDemand',
),
);
/*
* Voltage
*/
if (
Number.isFinite(voltage) &&
voltage > 0
) {
result.voltage =
Math.round(voltage * 10) / 10;
}
/*
* PI heating demand
*/
if (
Number.isFinite(demand) &&
demand >= 0 &&
demand <= 100
) {
result.pi_heating_demand =
Math.round(demand);
result.running_state =
demand >= 10
? 'heat'
: 'idle';
}
/*
* Current
*
* OTH3600:
*
* rmsCurrent / 100
*/
let current;
if (
Number.isFinite(rawCurrent) &&
rawCurrent >= 0
) {
current =
rawCurrent / 100;
if (
Number.isFinite(demand) &&
demand === 0
) {
current = 0;
}
result.current =
Math.round(current * 100) / 100;
}
/*
* Power
*
* Electric floor heating is essentially resistive:
*
* P = V × I
*/
if (
Number.isFinite(voltage) &&
voltage > 0 &&
Number.isFinite(current)
) {
let power =
voltage * current;
if (
Number.isFinite(demand) &&
demand === 0
) {
power = 0;
}
result.power =
Math.round(power);
}
return result;
}
/*
* ==========================================================================
* FROM ZIGBEE
* ==========================================================================
*/
const fzLocal = {
/*
* ----------------------------------------------------------------------
* Sinopé proprietary cluster
* ----------------------------------------------------------------------
*/
sinopeData: {
cluster: 'manuSpecificSinope',
type: [
'attributeReport',
'readResponse',
],
convert: (
model,
msg,
publish,
options,
meta,
) => {
const result = {};
/*
* Outdoor temperature
*/
if (
msg.data.outdoorTempToDisplay !==
undefined
) {
result.thermostat_outdoor_temperature =
msg.data.outdoorTempToDisplay /
100;
}
/*
* Outdoor temperature timeout
*/
if (
msg.data.outdoorTempToDisplayTimeout !==
undefined
) {
result.outdoor_temperature_timeout =
msg.data.outdoorTempToDisplayTimeout;
}
/*
* Secondary display mode
*/
if (
msg.data.secondScreenBehavior !==
undefined
) {
const lookup = {
0: 'auto',
1: 'setpoint',
2: 'outdoor temp',
};
result.second_display_mode =
lookup[
msg.data.secondScreenBehavior
];
}
return result;
},
},
/*
* ----------------------------------------------------------------------
* Electrical measurements
* ----------------------------------------------------------------------
*/
electricalMeasurement: {
cluster: 'haElectricalMeasurement',
type: [
'attributeReport',
'readResponse',
],
convert: (
model,
msg,
publish,
options,
meta,
) => {
const values = {};
if (
msg.data.rmsVoltage !==
undefined
) {
values.voltage =
msg.data.rmsVoltage;
}
if (
msg.data.rmsCurrent !==
undefined
) {
values.rawCurrent =
msg.data.rmsCurrent;
}
return calculateElectrical(
msg.endpoint,
values,
);
},
},
/*
* ----------------------------------------------------------------------
* PI heating demand
* ----------------------------------------------------------------------
*/
heatingDemand: {
cluster: 'hvacThermostat',
type: [
'attributeReport',
'readResponse',
],
convert: (
model,
msg,
publish,
options,
meta,
) => {
if (
msg.data.pIHeatingDemand ===
undefined
) {
return {};
}
return calculateElectrical(
msg.endpoint,
{
demand:
Number(
msg.data.pIHeatingDemand
),
},
);
},
},
/*
* ----------------------------------------------------------------------
* Energy
* ----------------------------------------------------------------------
*
* seMetering.currentSummDelivered contains cumulative consumed energy.
*
* Zigbee formula:
*
* Energy =
* currentSummDelivered × multiplier / divisor
*
* If multiplier/divisor are not implemented by the OTH3600,
* the raw currentSummDelivered value is used.
*/
metering: {
cluster: 'seMetering',
type: [
'attributeReport',
'readResponse',
],
convert: (
model,
msg,
publish,
options,
meta,
) => {
const result = {};
if (
msg.data.currentSummDelivered !==
undefined
) {
const rawEnergy =
Number(
msg.data.currentSummDelivered
);
const multiplier =
Number(
getCached(
msg.endpoint,
'seMetering',
'multiplier',
),
);
const divisor =
Number(
getCached(
msg.endpoint,
'seMetering',
'divisor',
),
);
let factor = 1;
if (
Number.isFinite(multiplier) &&
multiplier > 0 &&
Number.isFinite(divisor) &&
divisor > 0
) {
factor =
multiplier / divisor;
}
if (
Number.isFinite(rawEnergy)
) {
result.energy =
Math.round(
rawEnergy *
factor *
1000
) / 1000;
}
}
return result;
},
},
};
/*
* ==========================================================================
* TO ZIGBEE
* ==========================================================================
*/
const tzLocal = {
/*
* ----------------------------------------------------------------------
* Outdoor temperature
* ----------------------------------------------------------------------
*/
thermostatOutdoorTemperature: {
key: [
'thermostat_outdoor_temperature',
],
convertSet: async (
entity,
key,
value,
meta,
) => {
const temperature =
Number(value);
if (
!Number.isFinite(temperature) ||
temperature < -99.5 ||
temperature > 99.5
) {
throw new Error(
'Outdoor temperature must be between -99.5 and 99.5 °C'
);
}
await entity.write(
'manuSpecificSinope',
{
outdoorTempToDisplay:
Math.round(
temperature * 100
),
},
manuSinope,
);
return {
state: {
thermostat_outdoor_temperature:
temperature,
},
};
},
convertGet: async (
entity,
key,
meta,
) => {
await entity.read(
'manuSpecificSinope',
[
'outdoorTempToDisplay',
],
manuSinope,
);
},
},
/*
* ----------------------------------------------------------------------
* Outdoor temperature timeout
* ----------------------------------------------------------------------
*/
outdoorTemperatureTimeout: {
key: [
'outdoor_temperature_timeout',
],
convertSet: async (
entity,
key,
value,
meta,
) => {
const timeout =
Number(value);
if (
!Number.isFinite(timeout) ||
timeout < 30 ||
timeout > 64800
) {
throw new Error(
'Outdoor temperature timeout must be between 30 and 64800 seconds'
);
}
await entity.write(
'manuSpecificSinope',
{
outdoorTempToDisplayTimeout:
Math.round(timeout),
},
manuSinope,
);
return {
state: {
outdoor_temperature_timeout:
timeout,
},
};
},
convertGet: async (
entity,
key,
meta,
) => {
await entity.read(
'manuSpecificSinope',
[
'outdoorTempToDisplayTimeout',
],
manuSinope,
);
},
},
/*
* ----------------------------------------------------------------------
* Secondary display mode
* ----------------------------------------------------------------------
*/
secondDisplayMode: {
key: [
'second_display_mode',
],
convertSet: async (
entity,
key,
value,
meta,
) => {
const lookup = {
auto: 0,
setpoint: 1,
'outdoor temp': 2,
};
if (
lookup[value] === undefined
) {
throw new Error(
'second_display_mode must be auto, setpoint or outdoor temp'
);
}
await entity.write(
'manuSpecificSinope',
{
secondScreenBehavior:
lookup[value],
},
manuSinope,
);
return {
state: {
second_display_mode:
value,
},
};
},
convertGet: async (
entity,
key,
meta,
) => {
await entity.read(
'manuSpecificSinope',
[
'secondScreenBehavior',
],
manuSinope,
);
},
},
/*
* ----------------------------------------------------------------------
* Electrical / Energy refresh
* ----------------------------------------------------------------------
*/
electricalMeasurements: {
key: [
'voltage',
'current',
'power',
'energy',
'pi_heating_demand',
'running_state',
],
convertGet: async (
entity,
key,
meta,
) => {
/*
* Voltage + current
*/
try {
await entity.read(
'haElectricalMeasurement',
[
'rmsVoltage',
'rmsCurrent',
],
);
} catch {
// Ignore unsupported read
}
/*
* PI heating demand
*/
try {
await entity.read(
'hvacThermostat',
[
'pIHeatingDemand',
],
);
} catch {
// Ignore unsupported read
}
/*
* Metering multiplier / divisor.
*
* Read separately because the OTH3600 may not
* implement them.
*/
try {
await entity.read(
'seMetering',
[
'multiplier',
'divisor',
],
);
} catch {
// Raw energy value will be used.
}
/*
* Cumulative consumed energy.
*/
try {
await entity.read(
'seMetering',
[
'currentSummDelivered',
],
);
} catch {
// Ignore unsupported read
}
},
},
};
/*
* ==========================================================================
* DEVICE DEFINITION
* ==========================================================================
*/
const definition = {
zigbeeModel: [
'OTH3600-GA-ZB',
],
model:
'OTH3600-GA-ZB',
vendor:
'Ouellet',
description:
'Ouellet Zigbee floor heating thermostat',
/*
* Sinopé/Ouellet PI heating demand is already 0..100.
*/
meta: {
thermostat: {
dontMapPIHeatiSource: Koenkk/zigbee2mqtt