Sensor arcón congelador ZT08 e integración en Home Assistant

Hace unas semanas vi un termómetro zigbee con una sonda externa que me podia ir bien para el arcón congelador que tengo , el enlace de Aliexpres es este

Lo elegí porque el que tenia antes a partir de -20ºC tenia problemas y no daba lectura , el mínimo teórico de este dispositivo son -40ºC

Se puede colocar de diferentes maneras , desde magnética , adhesiva y con ventosa , al final me decante por la adhesiva en la tapa de las baterías.

Va alimentado por tres pilas AAA

Al alimentarlo ya nos muestra la temperatura local , la de las sonda , la hora y el nivel de bateria , pero las temperaturas en grados Fahrenheit

El siguiente paso es integrarlo en zigbee2mqtt , la información de este dispositivo será la siguiente.

Nada mas emparejarlo nos dice que no esta soportado , y nos devuelve este identificador “_TZE284_hodyryli

Para disponer de el tendremos que usar un converter , aquí encontre uno que funcionaba , el código seria el siguiente :

const tuya = require('zigbee-herdsman-converters/lib/tuya');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const e = exposes.presets;
const ea = exposes.access;

const fzLocal = {
    tuya_weather_station: {
        cluster: 'manuSpecificTuya',
        type: ['commandDataReport', 'commandActiveStatusReport', 'commandMcuSyncTime'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            
            // Time synchro
            if (msg.type === 'commandMcuSyncTime') {
                (async () => {
                    try {
                        const endpoint = msg.endpoint;
                        const now = new Date();
                        
                        const utcTime = Math.round(now.getTime() / 1000);
                        const localTime = utcTime - (now.getTimezoneOffset() * 60);
                        
                        const utcArray = [
                            (utcTime >> 24) & 0xFF,
                            (utcTime >> 16) & 0xFF,
                            (utcTime >> 8) & 0xFF,
                            utcTime & 0xFF,
                        ];
                        
                        const localArray = [
                            (localTime >> 24) & 0xFF,
                            (localTime >> 16) & 0xFF,
                            (localTime >> 8) & 0xFF,
                            localTime & 0xFF,
                        ];
                        
                        const payload = {
                            payloadSize: 8,
                            payload: [...utcArray, ...localArray],
                        };
                        
                        await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {disableDefaultResponse: true});
                        
                        // set 24h time format
                        await new Promise(resolve => setTimeout(resolve, 500));
                        try {
                            await tuya.sendDataPointBool(endpoint, 17, false);
                        } catch (e) {
                            // Ignore if DP 17 don't exist
                        }
                        
                    } catch (error) {
                        // Ignore time synchro errors
                    }
                })();
                
                return {};
            }
            
            if (!msg.data || !msg.data.dpValues) return {};
            
            for (const dpValue of msg.data.dpValues) {
                const dp = dpValue.dp;
                const data = dpValue.data;
                
                let value;
                // temperature
                if (dpValue.datatype === 2) {
                    // Reads the data as an unsigned big-endian 32-bit integer (Negative numbers will be wrong, eg. 429496724.6)
                    // value = data.readUInt32BE(0);
                    // Reads the data as a signed big-endian 32-bit integer (Negative numbers will be correct)
                    value = data.readInt32BE(0);
                } else if (dpValue.datatype === 4) {
                    value = data[0];
                } else {
                    value = data[0];
                }
                
                switch (dp) {
                    // temperature
                    case 1:
                        result.temperature = value / 10;
                        break;
                    // humidity
                    case 2:
                        result.humidity = value;
                        break;
                    // battery_state
                    case 3:
                        result.battery_state = value;
                        result.battery = value === 0 ? 10 : (value === 1 ? 50 : 100);
                        result.battery_low = value === 0;
                        break;
                    // time_format
                    case 17:
                        result.time_format = value ? '12h' : '24h';
                        break;
                    // temperature_external
                    case 38:
                        result.temperature_external = value / 10;
                        break;
                }
            }
            
            return result;
        },
    },
};

const definition = {
    fingerprint: tuya.fingerprint('TS0601', ['_TZE284_hodyryli']),
    model: 'TS0601_weather_station',
    vendor: 'TuYa',
    description: 'Weather station with clock, internal/external temperature and humidity',
    fromZigbee: [fzLocal.tuya_weather_station],
    toZigbee: [tuya.tz.datapoints],
    configure: tuya.configureMagicPacket,
    exposes: [
        e.temperature().withDescription('Internal temperature'),
        e.humidity().withDescription('Internal humidity'),
        e.numeric('temperature_external', ea.STATE)
            .withUnit('°C')
            .withDescription('External temperature sensor'),
        e.battery()
            .withDescription('Battery level (10%=low, 50%=medium, 100%=full)'),
        e.battery_low()
            .withDescription('Battery low warning'),
        e.enum('battery_state', ea.STATE, [0, 1, 2])
            .withDescription('Raw battery state (0=low, 1=medium, 2=full)'),
        e.enum('time_format', ea.STATE, ['12h', '24h'])
            .withDescription('Clock time format'),
    ],
    meta: {
        tuyaDatapoints: [
            [1, 'temperature', tuya.valueConverter.divideBy10],
            [2, 'humidity', tuya.valueConverter.raw],
            [3, 'battery_state', tuya.valueConverter.raw],
            [17, 'time_format', tuya.valueConverter.onOff],
            [38, 'temperature_external', tuya.valueConverter.divideBy10],
        ],
    },
};

module.exports = definition;

Crearemos el fichero TS0601.js y copiamos el código en el

paramos el docker

en configuration.yaml añadimos el fichero del converter

groups: {}
external_converters:
  - TS0601.js
  - TS0202.js

Arrancamos el docker y ya nos aparece el nuevo dispositivo

Ya podemos ver como empieza a exponer los datos

Físicamente una vez colocado quedaria así , con los grados en Fahrenheit 🙁

  '0xa4c138f9601e97ca':
    friendly_name: '0xa4c138f9601e97ca'
    temperature_calibration: 0
    temperature_precision: 1
    humidity_calibration: 0
    humidity_precision: 0

Crearemos nuestros sensores

  ### TERMOMETRO ARCON CONGELADOR
  
    - state_topic: "zigbee2mqtt/temperatura_congelador"
      availability_topic: "zigbee2mqtt/bridge/state"
      unit_of_measurement: "°C"
      device_class: "temperature"
      value_template: "{{ value_json.temperature }}"
      name: "temperatura_lavadero"  
      
    - state_topic: "zigbee2mqtt/temperatura_congelador"
      availability_topic: "zigbee2mqtt/bridge/state"
      unit_of_measurement: "°C"
      device_class: "temperature"
      value_template: "{{ value_json.temperature_external }}"
      name: "temperatura_congelador_temperatura"  
  
    - state_topic: "zigbee2mqtt/temperatura_congelador"
      availability_topic: "zigbee2mqtt/bridge/state"
      unit_of_measurement: "%"
      device_class: "humidity"
      value_template: "{{ value_json.humidity }}"
      name: "temperatura_congelador_humedad"  
  
    - state_topic: "zigbee2mqtt/temperatura_congelador"
      availability_topic: "zigbee2mqtt/bridge/state"
      unit_of_measurement: "%"
      icon: "mdi:battery"
      device_class: "battery"
      value_template: "{{ value_json.battery }}"
    
      expire_after: 86400
      force_update: true
      name: "temperatura_congelador_bateria"  
  
    - state_topic: "zigbee2mqtt/temperatura_congelador"
      availability_topic: "zigbee2mqtt/bridge/state"
      icon: "mdi:signal"
      unit_of_measurement: "lqi"
      value_template: "{{ value_json.linkquality }}"    
      name: "temperatura_congelador_estado"  
      
    - state_topic: "zigbee2mqtt/temperatura_congelador"
      availability_topic: "zigbee2mqtt/bridge/state"
      icon: "mdi:calendar-clock"
      value_template: "{{ value_json.last_seen }}"
      name: "temperatura_congelador_ultima_conexion" 

Y después de reiniciar veremos como va guardando los diferentes valores

Y con esto y un bizcocho …………

BONUS : Me tocaba bastante los webs ver los grados en Fahrenheit por lo que mire a ver como ponerlos en Celsius

Para ello cambiaremos el código del converter por este

const tuya = require('zigbee-herdsman-converters/lib/tuya');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const e = exposes.presets;
const ea = exposes.access;

const fzLocal = {
    tuya_weather_station: {
        cluster: 'manuSpecificTuya',
        type: ['commandDataReport', 'commandActiveStatusReport', 'commandMcuSyncTime'],
        convert: (model, msg, publish, options, meta) => {
            const result = {};
            
            // Time synchro
            if (msg.type === 'commandMcuSyncTime') {
                (async () => {
                    try {
                        const endpoint = msg.endpoint;
                        const now = new Date();
                        
                        const utcTime = Math.round(now.getTime() / 1000);
                        const localTime = utcTime - (now.getTimezoneOffset() * 60);
                        
                        const utcArray = [
                            (utcTime >> 24) & 0xFF,
                            (utcTime >> 16) & 0xFF,
                            (utcTime >> 8) & 0xFF,
                            utcTime & 0xFF,
                        ];
                        
                        const localArray = [
                            (localTime >> 24) & 0xFF,
                            (localTime >> 16) & 0xFF,
                            (localTime >> 8) & 0xFF,
                            localTime & 0xFF,
                        ];
                        
                        const payload = {
                            payloadSize: 8,
                            payload: [...utcArray, ...localArray],
                        };
                        
                        await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {disableDefaultResponse: true});
                        
                        // set 24h time format
                        await new Promise(resolve => setTimeout(resolve, 500));
                        try {
                            await tuya.sendDataPointBool(endpoint, 17, false);
                        } catch (e) {
                            // Ignore if DP 17 don't exist
                        }
                        
                    } catch (error) {
                        // Ignore time synchro errors
                    }
                })();
                
                return {};
            }
            
            if (!msg.data || !msg.data.dpValues) return {};
            
            for (const dpValue of msg.data.dpValues) {
                const dp = dpValue.dp;
                const data = dpValue.data;
                
                let value;
                // temperature
                if (dpValue.datatype === 2) {
                    // Reads the data as an unsigned big-endian 32-bit integer (Negative numbers will be wrong, eg. 429496724.6)
                    // value = data.readUInt32BE(0);
                    // Reads the data as a signed big-endian 32-bit integer (Negative numbers will be correct)
                    value = data.readInt32BE(0);
                } else if (dpValue.datatype === 4) {
                    value = data[0];
                } else {
                    value = data[0];
                }
                
                switch (dp) {
                    // temperature
                    case 1:
                        result.temperature = value / 10;
                        break;
                    // humidity
                    case 2:
                        result.humidity = value;
                        break;
                    // battery_state
                    case 3:
                        result.battery_state = value;
                        result.battery = value === 0 ? 10 : (value === 1 ? 50 : 100);
                        result.battery_low = value === 0;
                        break;
                    // temperature_unit (LCD display)
                    case 9:
                        result.temperature_unit = value === 0 ? 'celsius' : 'fahrenheit';
                        break;
                    // time_format
                    case 17:
                        result.time_format = value ? '12h' : '24h';
                        break;
                    // temperature_external
                    case 38:
                        result.temperature_external = value / 10;
                        break;
                }
            }
            
            return result;
        },
    },
};

const definition = {
    fingerprint: tuya.fingerprint('TS0601', ['_TZE284_hodyryli']),
    model: 'TS0601_weather_station',
    vendor: 'TuYa',
    description: 'Weather station with clock, internal/external temperature and humidity',
    fromZigbee: [fzLocal.tuya_weather_station],
    toZigbee: [tuya.tz.datapoints],
    configure: tuya.configureMagicPacket,
    exposes: [
        e.temperature().withDescription('Internal temperature'),
        e.humidity().withDescription('Internal humidity'),
        e.numeric('temperature_external', ea.STATE)
            .withUnit('°C')
            .withDescription('External temperature sensor'),
        e.battery()
            .withDescription('Battery level (10%=low, 50%=medium, 100%=full)'),
        e.battery_low()
            .withDescription('Battery low warning'),
        e.enum('battery_state', ea.STATE, [0, 1, 2])
            .withDescription('Raw battery state (0=low, 1=medium, 2=full)'),
        e.enum('temperature_unit', ea.STATE_SET, ['celsius', 'fahrenheit'])
            .withDescription('Temperature unit displayed on LCD screen'),
        e.enum('time_format', ea.STATE, ['12h', '24h'])
            .withDescription('Clock time format'),
    ],
    meta: {
        tuyaDatapoints: [
            [1, 'temperature', tuya.valueConverter.divideBy10],
            [2, 'humidity', tuya.valueConverter.raw],
            [3, 'battery_state', tuya.valueConverter.raw],
            [9, 'temperature_unit', tuya.valueConverter.temperatureUnitEnum],
            [17, 'time_format', tuya.valueConverter.onOff],
            [38, 'temperature_external', tuya.valueConverter.divideBy10],
        ],
    },
};

module.exports = definition;

Por defecto lo tendremos asi

Al consultar los ajustes nos aparece un nuevo selector Celsius / Fahrenheit

Cambiamos a Celsius y pulsamos varias veces para que se actualice

Y tachannnnnnnnnnnnnn ya aparece en Celsius

Gateway DGNWG02LM obtener token y key , e integración en Home Assistant

Hace poco cayo en mis manos un Gateway DGNWG02LM , ya tenia tres que integre en su dia y la verdad es que funcionan muy bien , tanto como luz de cortesia , para avisos visuales y avisos sonoros , y lo fui a integrar como se hacia hace unos años , pero mis coj .. veintitrés.

Antes mediante la aplicación Mi Home obtenías la KEY y la usabas directamente , pero ahora no hay forma directa de hacerlo y telita con la nueva forma de hacerlo , a ello se le suma que con los nuevos firmwares han bloqueado el puerto al que se accedia directamente para tener control via LAN , vamos una tocada de webs.

Yo uso Mi Home VEVS para saltarme las restricciones geográficas en mis dispositivos

Lo primero será sacar el token con VEVS

Ya tenemos el token , en este caso será el d3c1b247bfebdac74a25c5db89726a60

Ahora tenemos que poner nosotros una key a nuestra elección de 16 caracteres alfanumérica , lo haremos siguiendo este proyecto https://github.com/rytilahti/python-miio

Para ahorrarme problemas de versiones de Python y demás tonterias lo instalare en el docker de mi Home Assistant

pip install python-miio

Una vez instalado ejecutaremos el siguiente comando

miiocli gateway --ip 192.168.1.45 --token d3c1b247bfebdac74a25c5db89726a60 set_developer_key 0123456789012345

0123456789012345 será la key que le asignare y el Gateway lo había configurado en la IP 192.168.1.45

Si devuelve el OK entonces vamos por el buen camino

Esto serian los parámetros que le pondremos en nuestro secrets.yaml

######## GATEWAY XIAOMI 4 ######## 
gateway_4_key: 0123456789012345
gateway_4_mac: 04CF8CF69F4F

Vamos a ver como esta el tema de puertos , ejecutamos el siguiente comando

miiocli -d gateway --ip 192.168.1.45 --token d3c1b247bfebdac74a25c5db89726a60 enable_telnet

Y vemos que la respuesta nos da un error , malooooooo , muy malooooooooooooooooo

Para probar el puerto ejecutamos este comando de nmap

sudo nmap -sU 192.168.1.45 -p 9898

y vemos como nos dice que esta cerrado el puerto

No queda otra opción que abrir el cacharro , para ello seguiremos el manual de https://community.openhab.org/t/solved-openhab2-xiaomi-mi-gateway-does-not-respond/52963/171

Tiene tres tornillos en la parte inferior que debemos quitar

Estos tornillos tienen una forma bien curiosa

Una vez abierto separamos al parte del altavoz

Al no tener comunicación via wifi no nos queda mas remedio que conectarnos via UART , tendremos que soldar SOLO TRES CABLES , RX , TX y GND , no soldar la alimentación ya que tendremos que conectar el dispositivo a 220 mientras nos comunicamos con el

GND lo podemos coger de la carcasa del pulsador

Otra opción seria coger GND del conector directamente

Yo para comunicarme prefiero usar el hyperterminal de toda la vida , pero en las ultimas versiones de Windows no viene , pero lo podemos descargar de aquí mismo

Conectamos el Gateway y el adaptador USB a RS232

Los parámetros de comunicación son 115200, 8,N,1,N

Enviamos el comando  psm-set network.open_pf 3 , y lo comprobamos con el comando   psm-get network.open_pf para asegurarnos de que esta abierto

reiniciamos el gateway y si queremos opcionalmente podemos comprobar que el puerto 4321 esta abierto con el comando nmap -sU -p 4321 192.168.1.45

Al entrar en Home Assistant vemos como nos lo ha detectado ya

Ahora es donde tenemos que poner la KEY que habíamos grabado en el cacharro

Si todo es correcto nos lo reconoce al momento

Ya nos aparece el dispositivo con su MAC, en este caso seria light.gateway_light_04cf8cf69f4f

Lo ponemos bonito en el customize.yaml

light.gateway_light_04cf8cf69f4f:
  friendly_name: Luz Gateway 4
  icon: mdi:blur-radial

Ahora ya podemos empezar a trastear con el en nuestras automatizaciones , en este caso lo añadiré a la de parada de la caldera por apertura de puertas o ventanas

# ================================
# 🛑 DESACTIVACIÓN POR PUERTAS/VENTANAS
# ================================

- id: apagar_calefaccion_puertas_balcones_abiertas_5min
  alias: "🚪 Apagar Calefacción - Puertas Balcones Abiertas 5min"
  description: "Apaga la calefacción si puertas/balcones están abiertos más de 5 minutos"
  initial_state: true
  mode: restart
  trigger:
    - platform: state
      entity_id:
        - binary_sensor.sensor_balcon_comedor_derecho_evento
        - binary_sensor.sensor_balcon_comedor_izquierdo_evento
        - binary_sensor.sensor_balcon_matrimonio_derecho_evento
        - binary_sensor.sensor_balcon_matrimonio_izquierdo_evento
        - binary_sensor.sensor_oriol_derecho_evento
        - binary_sensor.sensor_oriol_izquierdo_evento
      to: 'on'
      for:
        minutes: 5
  condition:
    - condition: state
      entity_id: climate.calefaccion_casa
      state: heat
  action:
    - service: climate.turn_off
      target:
        entity_id: climate.calefaccion_casa
    
    - service: notify.notif_telegram_bot
      data:
        message: |
          ❄️🚪 *Calefacción APAGADA*
          
          ⚠️ Puerta/balcón abierto >5min
          🚪 Sensor: {{ trigger.to_state.attributes.friendly_name }}
          🌡️ Temperatura: {{ states('sensor.temperatura_comedor_calibrada') }}°C

    - service: light.turn_on
      entity_id: light.gateway_light_7c49eb1d1d0a
      data:
        brightness: 255
        rgb_color: [4, 29, 247] 
    - service: light.turn_on
      entity_id: light.gateway_light_286c07f0e736
      data:
        brightness: 255
        rgb_color: [4, 29, 247]       
    - service: light.turn_on
      entity_id: light.gateway_light_286c07f0b574
      data:
        brightness: 255
        rgb_color: [4, 29, 247] 
    - service: light.turn_on
      entity_id: light.gateway_light_04cf8cf69f4f
      data:
        brightness: 255
        rgb_color: [4, 29, 247] 
   
    # TTS solo entre las 8:00 y las 23:00
    - choose:
        - conditions:
            - condition: time
              after: "08:00:00"
              before: "23:00:00"
          sequence:
            - service: tts.google_translate_say
              data:
                language: "es-es"	
                entity_id: 
                  - media_player.lenovo_smart_clock
                  - media_player.lenovo_lenovo_matrimonio
                  - media_player.googlehome2670
                message: 'Apagado de la caldera , alguna puerta abierta'   
        
        
    #Apagamos las luces azules a los quince minutos        
    - delay: 00:15:00

    - service: light.turn_off
      entity_id: light.gateway_light_7c49eb1d1d0a
    - service: light.turn_off
      entity_id: light.gateway_light_286c07f0e736        
    - service: light.turn_off
      entity_id: light.gateway_light_286c07f0b574  
    - service: light.turn_off
      entity_id: light.gateway_light_04cf8cf69f4f  

Y con esto y un bizcocho ……