Skip to content

[External Converter]: Cool.stick from Sprut.device #30425

@alexij9-star

Description

@alexij9-star

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?

Cool Stick.txt

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:

  1. Auxiliary switches (endpoints 2-9): Switches for ECO mode, Self-cleaning, Sterilization, etc., do not respond to commands or update their state.
  2. Swing mode control: The custom swing mode attribute (0x6666) with manufacturer code 0x6666 doesn't work properly.
  3. Temperature setpoints: Changing temperature values has significant delays or is sometimes ignored.
  4. 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:

  1. Switch endpoints (2-9): Commands are sent but don't affect the device. The getEndpoint() calls might be failing.
  2. Custom attribute 0x6666: Reading/writing with manufacturer code doesn't work as expected.
  3. Reporting configuration: Some attributes might not be reporting correctly.
  4. 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:

  1. Proper communication with switch endpoints (2-9)
  2. Handling the custom manufacturer-specific attributes
  3. Setting up correct reporting

Thank you for your assistance!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions