-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Description
Link
https://sprut.ai/catalog/item/defaro-coolstick?ysclid=mjvp6tsdfl945062991
Database entry
{"id":34,"type":"Router","ieeeAddr":"0x84ba20fffe7d2818","nwkAddr":64725,"manufId":26214,"manufName":"Sprut.device","modelId":"Cool.stick","epList":[1,2,3,4,5,6,7,8,9,10],"endpoints":{"1":{"profId":260,"epId":1,"devId":769,"inClusterList":[0,513,514,1026,26112],"outClusterList":[25],"clusters":{"genBasic":{"attributes":{"modelId":"Cool.stick","manufacturerName":"Sprut.device","zclVersion":8,"appVersion":27,"hwVersion":1,"swBuildId":"27.80.195"}},"msTemperatureMeasurement":{"attributes":{"measuredValue":2500}},"hvacFanCtrl":{"attributes":{"26112":0,"fanMode":2}},"hvacThermostat":{"attributes":{"systemMode":0,"occupiedCoolingSetpoint":2500,"occupiedHeatingSetpoint":2500,"runningMode":0,"localTemp":2500}}},"binds":[{"cluster":1026,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1},{"cluster":513,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1},{"cluster":514,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":1026,"attrId":0,"minRepIntval":10,"maxRepIntval":3600,"repChange":100},{"cluster":513,"attrId":30,"minRepIntval":10,"maxRepIntval":300,"repChange":1},{"cluster":513,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":10},{"cluster":513,"attrId":17,"minRepIntval":0,"maxRepIntval":3600,"repChange":10},{"cluster":513,"attrId":18,"minRepIntval":0,"maxRepIntval":3600,"repChange":10},{"cluster":513,"attrId":28,"minRepIntval":10,"maxRepIntval":3600,"repChange":null},{"cluster":514,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"2":{"profId":260,"epId":2,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":1}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"3":{"profId":260,"epId":3,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"4":{"profId":260,"epId":4,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"5":{"profId":260,"epId":5,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"6":{"profId":260,"epId":6,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"7":{"profId":260,"epId":7,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"8":{"profId":260,"epId":8,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"9":{"profId":260,"epId":9,"devId":2,"inClusterList":[6],"outClusterList":[],"clusters":{"genOnOff":{"attributes":{"onOff":0}}},"binds":[{"cluster":6,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":6,"attrId":0,"minRepIntval":0,"maxRepIntval":3600,"repChange":0}],"meta":{}},"10":{"profId":260,"epId":10,"devId":770,"inClusterList":[1026],"outClusterList":[],"clusters":{"msTemperatureMeasurement":{"attributes":{"measuredValue":-1200}}},"binds":[{"cluster":1026,"type":"endpoint","deviceIeeeAddress":"0x20a716fffe22cc34","endpointID":1}],"configuredReportings":[{"cluster":1026,"attrId":0,"minRepIntval":10,"maxRepIntval":3600,"repChange":100}],"meta":{}}},"appVersion":27,"hwVersion":1,"swBuildId":"27.80.195","zclVersion":8,"interviewCompleted":true,"interviewState":"SUCCESSFUL","meta":{"configured":105725200},"lastSeen":1767287256400}
Zigbee2MQTT version
2.7.2 (unknown)
External converter
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const e = exposes.presets;
const ea = exposes.access;
const manufacturerCode = 0x6666;
const fzLocal = {
sprut_thermostat: {
cluster: 'hvacThermostat',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
const data = msg.data;
if (data.hasOwnProperty('localTemperature')) {
result.current_temperature = data['localTemperature'] / 100;
}
if (data.hasOwnProperty('occupiedCoolingSetpoint')) {
result.occupied_cooling_setpoint = data['occupiedCoolingSetpoint'] / 100;
}
if (data.hasOwnProperty('occupiedHeatingSetpoint')) {
result.occupied_heating_setpoint = data['occupiedHeatingSetpoint'] / 100;
}
if (data.hasOwnProperty('systemMode')) {
const systemMode = data['systemMode'];
const modeMap = {
0: 'off',
1: 'auto',
3: 'cool',
4: 'heat',
7: 'fan_only',
8: 'dry',
};
result.system_mode = modeMap[systemMode] || 'off';
}
if (data.hasOwnProperty('runningMode')) {
const runningMode = data['runningMode'];
result.running_state = runningMode === 0 ? 'idle' :
runningMode === 1 ? 'cool' :
runningMode === 2 ? 'heat' :
runningMode === 3 ? 'fan_only' : 'idle';
}
return result;
},
},
sprut_fan_control: {
cluster: 'hvacFanCtrl',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
const data = msg.data;
if (data.hasOwnProperty('fanMode')) {
const fanMode = data['fanMode'];
const fanModeMap = {
1: 'low',
2: 'medium',
3: 'high',
4: 'auto',
};
result.fan_mode = fanModeMap[fanMode] || 'auto';
}
// Проверяем кастомные атрибуты
for (const key in data) {
if (parseInt(key) === 0x6666) {
const swingMode = data[key];
result.swing_mode = swingMode === 0 ? 'disabled' :
swingMode === 1 ? 'enabled' :
swingMode === 2 ? 'horizontal' :
swingMode === 3 ? 'vertical' : 'disabled';
break;
}
}
return result;
},
},
sprut_on_off: {
cluster: 'genOnOff',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.hasOwnProperty('onOff')) {
const endpoint = msg.endpoint.ID;
const switchMap = {
2: 'indicator',
3: 'health_mode',
4: 'quick_mode',
5: 'quiet_mode',
6: 'sleep_mode',
7: 'mute',
8: 'self_clean',
9: 'sterilization',
};
const switchName = switchMap[endpoint];
if (switchName) {
result[switchName] = msg.data['onOff'] ? 'ON' : 'OFF';
}
}
return result;
},
},
sprut_temperature: {
cluster: 'msTemperatureMeasurement',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.hasOwnProperty('measuredValue') && msg.endpoint.ID === 10) {
const temp = msg.data['measuredValue'] / 100;
if (temp > -100) { // Игнорируем невалидные значения
result.external_temperature = temp;
}
}
return result;
},
},
};
const tzLocal = {
sprut_system_mode: {
key: ['system_mode'],
convertSet: async (entity, key, value, meta) => {
const modeMap = {
'off': 0,
'auto': 1,
'cool': 3,
'heat': 4,
'fan_only': 7,
'dry': 8,
};
const modeValue = modeMap[value];
if (modeValue === undefined) {
throw new Error(`Invalid system mode: ${value}`);
}
await entity.write('hvacThermostat', {systemMode: modeValue});
return {state: {system_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['systemMode']);
},
},
sprut_cooling_setpoint: {
key: ['occupied_cooling_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 100);
await entity.write('hvacThermostat', {occupiedCoolingSetpoint: temp});
return {state: {occupied_cooling_setpoint: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['occupiedCoolingSetpoint']);
},
},
sprut_heating_setpoint: {
key: ['occupied_heating_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 100);
await entity.write('hvacThermostat', {occupiedHeatingSetpoint: temp});
return {state: {occupied_heating_setpoint: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['occupiedHeatingSetpoint']);
},
},
sprut_fan_mode: {
key: ['fan_mode'],
convertSet: async (entity, key, value, meta) => {
const fanModeMap = {
'low': 1,
'medium': 2,
'high': 3,
'auto': 4,
};
const fanModeValue = fanModeMap[value];
if (fanModeValue === undefined) {
throw new Error(`Invalid fan mode: ${value}`);
}
await entity.write('hvacFanCtrl', {fanMode: fanModeValue});
return {state: {fan_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacFanCtrl', ['fanMode']);
},
},
sprut_swing_mode: {
key: ['swing_mode'],
convertSet: async (entity, key, value, meta) => {
const swingModeMap = {
'disabled': 0,
'enabled': 1,
'horizontal': 2,
'vertical': 3,
};
const swingModeValue = swingModeMap[value];
if (swingModeValue === undefined) {
throw new Error(`Invalid swing mode: ${value}`);
}
await entity.write('hvacFanCtrl', {0x6666: swingModeValue}, {manufacturerCode});
return {state: {swing_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacFanCtrl', [0x6666], {manufacturerCode});
},
},
sprut_switch: {
key: ['indicator', 'health_mode', 'quick_mode', 'quiet_mode', 'sleep_mode', 'mute', 'self_clean', 'sterilization'],
convertSet: async (entity, key, value, meta) => {
const endpointMap = {
'indicator': 2,
'health_mode': 3,
'quick_mode': 4,
'quiet_mode': 5,
'sleep_mode': 6,
'mute': 7,
'self_clean': 8,
'sterilization': 9,
};
const endpointNum = endpointMap[key];
if (!endpointNum) return;
const state = value.toLowerCase() === 'on';
const endpoint = entity.getEndpoint(endpointNum);
if (endpoint) {
await endpoint.write('genOnOff', {onOff: state});
return {state: {[key]: value.toUpperCase()}};
}
},
convertGet: async (entity, key, meta) => {
const endpointMap = {
'indicator': 2,
'health_mode': 3,
'quick_mode': 4,
'quiet_mode': 5,
'sleep_mode': 6,
'mute': 7,
'self_clean': 8,
'sterilization': 9,
};
const endpointNum = endpointMap[key];
if (!endpointNum) return;
const endpoint = entity.getEndpoint(endpointNum);
if (endpoint) {
await endpoint.read('genOnOff', ['onOff']);
}
},
},
};
const definition = {
zigbeeModel: ['Cool.stick'],
model: 'Cool.stick',
vendor: 'Sprut.device',
description: 'Zigbee air conditioner controller',
fromZigbee: [
fzLocal.sprut_thermostat,
fzLocal.sprut_fan_control,
fzLocal.sprut_on_off,
fzLocal.sprut_temperature,
],
toZigbee: [
tzLocal.sprut_system_mode,
tzLocal.sprut_cooling_setpoint,
tzLocal.sprut_heating_setpoint,
tzLocal.sprut_fan_mode,
tzLocal.sprut_swing_mode,
tzLocal.sprut_switch,
],
exposes: [
e.climate()
.withSetpoint('occupied_cooling_setpoint', 16, 30, 0.5, ea.STATE_SET)
.withSetpoint('occupied_heating_setpoint', 16, 30, 0.5, ea.STATE_SET)
.withLocalTemperature(ea.STATE)
.withSystemMode(['off', 'auto', 'cool', 'heat', 'fan_only', 'dry'], ea.STATE_SET)
.withRunningState(['idle', 'cool', 'heat', 'fan_only'], ea.STATE)
.withFanMode(['low', 'medium', 'high', 'auto'], ea.STATE_SET),
e.enum('swing_mode', ea.STATE_SET, ['disabled', 'enabled', 'horizontal', 'vertical'])
.withDescription('Swing mode'),
e.binary('indicator', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Indicator light'),
e.binary('health_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Health mode'),
e.binary('quick_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Quick mode'),
e.binary('quiet_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Quiet mode'),
e.binary('sleep_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Sleep mode'),
e.binary('mute', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Mute'),
e.binary('self_clean', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Self-cleaning mode'),
e.binary('sterilization', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Sterilization mode'),
e.numeric('external_temperature', ea.STATE)
.withUnit('°C')
.withDescription('External temperature measurement'),
],
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint1 = device.getEndpoint(1);
if (!endpoint1) {
console.error('Endpoint 1 not found');
return;
}
try {
// Basic binding
await endpoint1.bind('hvacThermostat', coordinatorEndpoint);
await endpoint1.bind('hvacFanCtrl', coordinatorEndpoint);
// Configure reporting
await reporting.thermostatTemperature(endpoint1);
await reporting.thermostatOccupiedCoolingSetpoint(endpoint1);
await reporting.thermostatOccupiedHeatingSetpoint(endpoint1);
await reporting.thermostatSystemMode(endpoint1);
await reporting.fanMode(endpoint1);
// Bind and configure switches
for (let i = 2; i <= 9; i++) {
const ep = device.getEndpoint(i);
if (ep) {
await ep.bind('genOnOff', coordinatorEndpoint);
await reporting.onOff(ep);
}
}
// Bind and configure external temperature sensor
const endpoint10 = device.getEndpoint(10);
if (endpoint10) {
await endpoint10.bind('msTemperatureMeasurement', coordinatorEndpoint);
await reporting.temperature(endpoint10);
}
console.log('Sprut Cool.stick configured successfully');
} catch (error) {
console.error('Configuration error:', error.message);
}
},
endpoint: (device) => {
return {default: 1};
},
meta: {
multiEndpoint: true,
},
};
module.exports = definition;What does/doesn't work with the external definition?
Notes
[External Converter]: Cool.stick from Sprut.device
Device manufacturer: Sprut.device
Device model: Cool.stick
Device description: Zigbee multi-channel air conditioner controller with thermostat, 8 auxiliary switches, and external temperature sensor support
Zigbee model manufacturer: Cool.stick (from Model ID field)
Manufacturer Code: 0x6666 (26214)
Endpoints and clusters information:
{"1":{"clusters":{"input":["genBasic","hvacThermostat","hvacFanCtrl","msTemperatureMeasurement","26112"],"output":["genOta"]}},"2":{"clusters":{"input":["genOnOff"],"output":[]}},"3":{"clusters":{"input":["genOnOff"],"output":[]}},"4":{"clusters":{"input":["genOnOff"],"output":[]}},"5":{"clusters":{"input":["genOnOff"],"output":[]}},"6":{"clusters":{"input":["genOnOff"],"output":[]}},"7":{"clusters":{"input":["genOnOff"],"output":[]}},"8":{"clusters":{"input":["genOnOff"],"output":[]}},"9":{"clusters":{"input":["genOnOff"],"output":[]}},"10":{"clusters":{"input":["msTemperatureMeasurement"],"output":[]}}}Software build ID: 27.80.195
Date code: undefined
Problem Description
I successfully paired my Cool.stick device with Zigbee2MQTT, but several functions do not work correctly with my current external converter:
- Auxiliary switches (endpoints 2-9): Switches for ECO mode, Self-cleaning, Sterilization, etc., do not respond to commands or update their state.
- Swing mode control: The custom swing mode attribute (0x6666) with manufacturer code 0x6666 doesn't work properly.
- Temperature setpoints: Changing temperature values has significant delays or is sometimes ignored.
- General reliability: The device communication seems unstable compared to its performance in Sprut Hub.
Technical Information from Sprut Hub
I have comprehensive technical data from the official Sprut Hub software:
Device Structure (from Sprut Hub diagnostics):
Address: 84BA20FFFE7D2818/0AB7
Endpoint 1 (THERMOSTAT):
- Cluster 0x0201 (Thermostat): LocalTemperature, OccupiedCoolingSetpoint, OccupiedHeatingSetpoint, SystemMode, ThermostatRunningMode
- Cluster 0x0202 (FanControl): FanMode, custom attribute 0x6666 (SwingMode) with manufacturer code 0x6666
- Cluster 0x6600 (SprutDevice custom)
Endpoints 2-9 (ON_OFF_OUTPUT):
- Cluster 0x0006 (OnOff): Each endpoint controls a specific function:
* Endpoint 2: Indicator light
* Endpoint 3: Health mode
* Endpoint 4: Quick mode
* Endpoint 5: Quiet mode
* Endpoint 6: Sleep mode
* Endpoint 7: Mute
* Endpoint 8: Self-clean
* Endpoint 9: Sterilization
Endpoint 10 (TEMPERATURE_SENSOR):
- Cluster 0x0402 (TemperatureMeasurement): MeasuredValue
Key mappings from Sprut Hub template:
- SystemMode (0x001C): off(0), auto(1), cool(3), heat(4), fan_only(7), dry(8)
- FanMode (0x0000): low(1), medium(2), high(3), auto(4)
- SwingMode (0x6666): disabled(0), enabled(1), horizontal(2), vertical(3)
- All switches use standard OnOff cluster on endpoints 2-9
Full Sprut Hub Device Template:
{
"name": "@air_cond",
"manufacturer": "Sprut.device",
"model": "Cool.stick",
"manufacturerIds": ["Sprut.device"],
"modelIds": ["Cool.stick"],
"catalogId": 3544,
"init": [
{
"link": [
{
"endpoint": 1,
"cluster": "0201_Thermostat",
"attribute": [
"0000_LocalTemperature",
"001C_SystemMode",
"001E_ThermostatRunningMode",
"0011_OccupiedCoolingSetpoint"
]
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 1,
"cluster": "0202_FanControl",
"attribute": "0000_FanMode"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 1,
"cluster": "0202_FanControl",
"attribute": "6600_SwingMode",
"manufacturerCode": 26214
}
],
"report": true
},
{
"link": [
{
"endpoint": 2,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 3,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 4,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 5,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 6,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 7,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 8,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 9,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
],
"bind": true,
"report": true
},
{
"link": [
{
"endpoint": 10,
"cluster": "0402_TemperatureMeasurement",
"attribute": [
"0000_MeasuredValue"
]
}
],
"bind": true,
"report": true
}
],
"services": [
{
"type": "Thermostat",
"characteristics": [
{
"type": "CurrentTemperature",
"link": [
{
"endpoint": 1,
"cluster": "0201_Thermostat",
"attribute": "0000_LocalTemperature"
}
]
},
{
"type": "TargetTemperature",
"link": [
{
"endpoint": 1,
"cluster": "0201_Thermostat",
"attribute": "0011_OccupiedCoolingSetpoint"
}
],
"minValue": 16,
"maxValue": 30,
"minStep": 1
},
{
"type": "CurrentHeatingCoolingState",
"link": [
{
"endpoint": 1,
"cluster": "0201_Thermostat",
"attribute": "001E_ThermostatRunningMode"
}
]
},
{
"type": "TargetHeatingCoolingState",
"link": [
{
"endpoint": 1,
"cluster": "0201_Thermostat",
"attribute": "001C_SystemMode"
}
],
"validValues": "DRY,FAN_ONLY,HEAT,COOL,AUTO,OFF"
},
{
"type": "C_FanSpeed",
"link": [
{
"endpoint": 1,
"cluster": "0202_FanControl",
"attribute": "0000_FanMode"
}
],
"validValues": "LOW,MEDIUM,HIGH,AUTO"
},
{
"type": "SwingMode",
"link": [
{
"endpoint": 1,
"cluster": "0202_FanControl",
"attribute": "6600_SwingMode",
"manufacturerCode": 26214
}
],
"validValues": "SWING_ENABLED,SWING_DISABLED,SWING_HORIZONTAL,SWING_VERTICAL"
}
]
},
{
"name": "Индикация",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 2,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Функция Здоровье",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 3,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Режим Быстрый",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 4,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Режим Тихий",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 5,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "@sleep",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 6,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Без звука",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 7,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Самоочистка",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 8,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Стерилизация",
"type": "Switch",
"characteristics": [
{
"type": "On",
"link": [
{
"endpoint": 9,
"cluster": "0006_OnOff",
"attribute": "0000_OnOff"
}
]
}
]
},
{
"name": "Внешняя температура",
"type": "TemperatureSensor",
"characteristics": [
{
"type": "CurrentTemperature",
"link": [
{
"endpoint": 10,
"cluster": "0402_TemperatureMeasurement",
"attribute": "0000_MeasuredValue"
}
]
}
]
}
],
"options": [
{
"link": [
{
"endpoint": 1,
"cluster": "6600_SprutDevice",
"command": "0067_Debug",
"manufacturerCode": 26214
}
],
"name": "Отладка",
"type": "Integer",
"value": 0,
"values": [
{
"value": 0,
"name": "Выключено"
},
{
"value": 1,
"name": "Включено"
}
]
}
]
}My Converter Attempt
I've tried to create an external converter based on similar devices, but it has issues:
const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/toZigbee');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const e = exposes.presets;
const ea = exposes.access;
const manufacturerCode = 0x6666;
const fzLocal = {
sprut_thermostat: {
cluster: 'hvacThermostat',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
const data = msg.data;
if (data.hasOwnProperty('localTemperature')) {
result.current_temperature = data['localTemperature'] / 100;
}
if (data.hasOwnProperty('occupiedCoolingSetpoint')) {
result.occupied_cooling_setpoint = data['occupiedCoolingSetpoint'] / 100;
}
if (data.hasOwnProperty('occupiedHeatingSetpoint')) {
result.occupied_heating_setpoint = data['occupiedHeatingSetpoint'] / 100;
}
if (data.hasOwnProperty('systemMode')) {
const systemMode = data['systemMode'];
const modeMap = {
0: 'off',
1: 'auto',
3: 'cool',
4: 'heat',
7: 'fan_only',
8: 'dry',
};
result.system_mode = modeMap[systemMode] || 'off';
}
if (data.hasOwnProperty('runningMode')) {
const runningMode = data['runningMode'];
result.running_state = runningMode === 0 ? 'idle' :
runningMode === 1 ? 'cool' :
runningMode === 2 ? 'heat' :
runningMode === 3 ? 'fan_only' : 'idle';
}
return result;
},
},
sprut_fan_control: {
cluster: 'hvacFanCtrl',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
const data = msg.data;
if (data.hasOwnProperty('fanMode')) {
const fanMode = data['fanMode'];
const fanModeMap = {
1: 'low',
2: 'medium',
3: 'high',
4: 'auto',
};
result.fan_mode = fanModeMap[fanMode] || 'auto';
}
// Проверяем кастомные атрибуты
for (const key in data) {
if (parseInt(key) === 0x6666) {
const swingMode = data[key];
result.swing_mode = swingMode === 0 ? 'disabled' :
swingMode === 1 ? 'enabled' :
swingMode === 2 ? 'horizontal' :
swingMode === 3 ? 'vertical' : 'disabled';
break;
}
}
return result;
},
},
sprut_on_off: {
cluster: 'genOnOff',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.hasOwnProperty('onOff')) {
const endpoint = msg.endpoint.ID;
const switchMap = {
2: 'indicator',
3: 'health_mode',
4: 'quick_mode',
5: 'quiet_mode',
6: 'sleep_mode',
7: 'mute',
8: 'self_clean',
9: 'sterilization',
};
const switchName = switchMap[endpoint];
if (switchName) {
result[switchName] = msg.data['onOff'] ? 'ON' : 'OFF';
}
}
return result;
},
},
sprut_temperature: {
cluster: 'msTemperatureMeasurement',
type: ['attributeReport', 'readResponse'],
convert: (model, msg, publish, options, meta) => {
const result = {};
if (msg.data.hasOwnProperty('measuredValue') && msg.endpoint.ID === 10) {
const temp = msg.data['measuredValue'] / 100;
if (temp > -100) { // Игнорируем невалидные значения
result.external_temperature = temp;
}
}
return result;
},
},
};
const tzLocal = {
sprut_system_mode: {
key: ['system_mode'],
convertSet: async (entity, key, value, meta) => {
const modeMap = {
'off': 0,
'auto': 1,
'cool': 3,
'heat': 4,
'fan_only': 7,
'dry': 8,
};
const modeValue = modeMap[value];
if (modeValue === undefined) {
throw new Error(`Invalid system mode: ${value}`);
}
await entity.write('hvacThermostat', {systemMode: modeValue});
return {state: {system_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['systemMode']);
},
},
sprut_cooling_setpoint: {
key: ['occupied_cooling_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 100);
await entity.write('hvacThermostat', {occupiedCoolingSetpoint: temp});
return {state: {occupied_cooling_setpoint: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['occupiedCoolingSetpoint']);
},
},
sprut_heating_setpoint: {
key: ['occupied_heating_setpoint'],
convertSet: async (entity, key, value, meta) => {
const temp = Math.round(value * 100);
await entity.write('hvacThermostat', {occupiedHeatingSetpoint: temp});
return {state: {occupied_heating_setpoint: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacThermostat', ['occupiedHeatingSetpoint']);
},
},
sprut_fan_mode: {
key: ['fan_mode'],
convertSet: async (entity, key, value, meta) => {
const fanModeMap = {
'low': 1,
'medium': 2,
'high': 3,
'auto': 4,
};
const fanModeValue = fanModeMap[value];
if (fanModeValue === undefined) {
throw new Error(`Invalid fan mode: ${value}`);
}
await entity.write('hvacFanCtrl', {fanMode: fanModeValue});
return {state: {fan_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacFanCtrl', ['fanMode']);
},
},
sprut_swing_mode: {
key: ['swing_mode'],
convertSet: async (entity, key, value, meta) => {
const swingModeMap = {
'disabled': 0,
'enabled': 1,
'horizontal': 2,
'vertical': 3,
};
const swingModeValue = swingModeMap[value];
if (swingModeValue === undefined) {
throw new Error(`Invalid swing mode: ${value}`);
}
await entity.write('hvacFanCtrl', {0x6666: swingModeValue}, {manufacturerCode});
return {state: {swing_mode: value}};
},
convertGet: async (entity, key, meta) => {
await entity.read('hvacFanCtrl', [0x6666], {manufacturerCode});
},
},
sprut_switch: {
key: ['indicator', 'health_mode', 'quick_mode', 'quiet_mode', 'sleep_mode', 'mute', 'self_clean', 'sterilization'],
convertSet: async (entity, key, value, meta) => {
const endpointMap = {
'indicator': 2,
'health_mode': 3,
'quick_mode': 4,
'quiet_mode': 5,
'sleep_mode': 6,
'mute': 7,
'self_clean': 8,
'sterilization': 9,
};
const endpointNum = endpointMap[key];
if (!endpointNum) return;
const state = value.toLowerCase() === 'on';
const endpoint = entity.getEndpoint(endpointNum);
if (endpoint) {
await endpoint.write('genOnOff', {onOff: state});
return {state: {[key]: value.toUpperCase()}};
}
},
convertGet: async (entity, key, meta) => {
const endpointMap = {
'indicator': 2,
'health_mode': 3,
'quick_mode': 4,
'quiet_mode': 5,
'sleep_mode': 6,
'mute': 7,
'self_clean': 8,
'sterilization': 9,
};
const endpointNum = endpointMap[key];
if (!endpointNum) return;
const endpoint = entity.getEndpoint(endpointNum);
if (endpoint) {
await endpoint.read('genOnOff', ['onOff']);
}
},
},
};
const definition = {
zigbeeModel: ['Cool.stick'],
model: 'Cool.stick',
vendor: 'Sprut.device',
description: 'Zigbee air conditioner controller',
fromZigbee: [
fzLocal.sprut_thermostat,
fzLocal.sprut_fan_control,
fzLocal.sprut_on_off,
fzLocal.sprut_temperature,
],
toZigbee: [
tzLocal.sprut_system_mode,
tzLocal.sprut_cooling_setpoint,
tzLocal.sprut_heating_setpoint,
tzLocal.sprut_fan_mode,
tzLocal.sprut_swing_mode,
tzLocal.sprut_switch,
],
exposes: [
e.climate()
.withSetpoint('occupied_cooling_setpoint', 16, 30, 0.5, ea.STATE_SET)
.withSetpoint('occupied_heating_setpoint', 16, 30, 0.5, ea.STATE_SET)
.withLocalTemperature(ea.STATE)
.withSystemMode(['off', 'auto', 'cool', 'heat', 'fan_only', 'dry'], ea.STATE_SET)
.withRunningState(['idle', 'cool', 'heat', 'fan_only'], ea.STATE)
.withFanMode(['low', 'medium', 'high', 'auto'], ea.STATE_SET),
e.enum('swing_mode', ea.STATE_SET, ['disabled', 'enabled', 'horizontal', 'vertical'])
.withDescription('Swing mode'),
e.binary('indicator', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Indicator light'),
e.binary('health_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Health mode'),
e.binary('quick_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Quick mode'),
e.binary('quiet_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Quiet mode'),
e.binary('sleep_mode', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Sleep mode'),
e.binary('mute', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Mute'),
e.binary('self_clean', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Self-cleaning mode'),
e.binary('sterilization', ea.STATE_SET, 'ON', 'OFF')
.withDescription('Sterilization mode'),
e.numeric('external_temperature', ea.STATE)
.withUnit('°C')
.withDescription('External temperature measurement'),
],
configure: async (device, coordinatorEndpoint, logger) => {
const endpoint1 = device.getEndpoint(1);
if (!endpoint1) {
console.error('Endpoint 1 not found');
return;
}
try {
// Basic binding
await endpoint1.bind('hvacThermostat', coordinatorEndpoint);
await endpoint1.bind('hvacFanCtrl', coordinatorEndpoint);
// Configure reporting
await reporting.thermostatTemperature(endpoint1);
await reporting.thermostatOccupiedCoolingSetpoint(endpoint1);
await reporting.thermostatOccupiedHeatingSetpoint(endpoint1);
await reporting.thermostatSystemMode(endpoint1);
await reporting.fanMode(endpoint1);
// Bind and configure switches
for (let i = 2; i <= 9; i++) {
const ep = device.getEndpoint(i);
if (ep) {
await ep.bind('genOnOff', coordinatorEndpoint);
await reporting.onOff(ep);
}
}
// Bind and configure external temperature sensor
const endpoint10 = device.getEndpoint(10);
if (endpoint10) {
await endpoint10.bind('msTemperatureMeasurement', coordinatorEndpoint);
await reporting.temperature(endpoint10);
}
console.log('Sprut Cool.stick configured successfully');
} catch (error) {
console.error('Configuration error:', error.message);
}
},
endpoint: (device) => {
return {default: 1};
},
meta: {
multiEndpoint: true,
},
};
module.exports = definition;Specific Issues with My Converter:
- Switch endpoints (2-9): Commands are sent but don't affect the device. The
getEndpoint()calls might be failing. - Custom attribute 0x6666: Reading/writing with manufacturer code doesn't work as expected.
- Reporting configuration: Some attributes might not be reporting correctly.
- Endpoint mapping: The multi-endpoint handling might be incorrect.
Additional Information:
- Zigbee2MQTT version: 2.7.2
- Coordinator: (Haier.)
- Device successfully paired: Yes
- Can provide debug logs: Yes, I can capture detailed debug logs for pairing and command attempts
- Manufacturer code: 0x6666 (26214) is confirmed from Node Descriptor
- OTA capability: Yes, endpoint 1 supports cluster 0x0019 (OtaUpgrade)
Request for Help:
Could you please help review my converter and identify the issues? The device works perfectly in Sprut Hub, so I believe it's a converter implementation problem. I'm ready to:
- Provide debug logs for any specific scenarios
- Test modified converter versions
- Share additional technical details from Sprut Hub
The main challenges seem to be:
- Proper communication with switch endpoints (2-9)
- Handling the custom manufacturer-specific attributes
- Setting up correct reporting
Thank you for your assistance!