After much deliberation, I’ve decided to try the Tasmota-IRHVAC and LG-Climate-component-for-Home-Assistant projects on GitHub, until Tuya official supports the integration in Home Assistant.
In the meanwhile, I was tinkering with some crude workarounds based on solution 3, which I thought I’d share for the benefit of anyone who would like to enhance it further.
Note: Home Assistant and Node-RED are Docker containers on a Raspberry Pi 4.
To start with, I created 4 Tap-to-run scenes in the Smart Life app: Switch on AC (which, if possible, I’d default to 20 degrees), Switch off AC, Increase AC temperature, and Decrease AC temperature.
Next, I restarted Home Assistant to sync these scenes via the Tuya Integration.
With these scenes now in Home Assistant, I created a new flow in Node-RED which tracks the triggers of these scenes, creates a JSON object to store the state and temperature of the AC, and saves this to a file in Node-RED’s volume under a custom_sensors
folder.
Node-RED flow:
[{"id":"b329f38a.b28f9","type":"tab","label":"Master Bedroom AC","disabled":false,"info":""},{"id":"5afab58201e0355a","type":"server-events","z":"b329f38a.b28f9","name":"","server":"461c4d2a.d4d3c4","version":1,"event_type":"call_service","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":110,"y":280,"wires":[["73a69158148ba100"]]},{"id":"81427887493dcaae","type":"function","z":"b329f38a.b28f9","name":"Transform Payload (Master Bedroom AC Turn On)","func":"msg.payload = { \"master_bedroom_ac\": { \"state\": \"on\", \"temperature\": 20 } };\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":200,"wires":[["f0d2632f67a083ad","78272c272af9bfa3"]]},{"id":"49e09c04a6203e89","type":"switch","z":"b329f38a.b28f9","name":"","property":"payload.event.service_data.entity_id[0]","propertyType":"msg","rules":[{"t":"eq","v":"scene.master_bedroom_ac_turn_on","vt":"str"},{"t":"eq","v":"scene.master_bedroom_ac_turn_off","vt":"str"},{"t":"eq","v":"scene.master_bedroom_ac_increase_temperature","vt":"str"},{"t":"eq","v":"scene.master_bedroom_ac_decrease_temperature","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":5,"x":310,"y":280,"wires":[["81427887493dcaae"],["22fff276e889ea6f"],["d4b62abb623a0a57"],["d4b62abb623a0a57"],["023f4b3f191f06a0"]]},{"id":"73a69158148ba100","type":"json","z":"b329f38a.b28f9","name":"","property":"payload","action":"obj","pretty":false,"x":230,"y":200,"wires":[["49e09c04a6203e89","6208f784bbd6b1d0"]]},{"id":"f0d2632f67a083ad","type":"file","z":"b329f38a.b28f9","name":"","filename":"/data/custom_sensors/sensor.master_bedroom_ac.json","appendNewline":true,"createDir":true,"overwriteFile":"true","encoding":"none","x":1170,"y":220,"wires":[[]]},{"id":"22fff276e889ea6f","type":"function","z":"b329f38a.b28f9","name":"Transform Payload (Master Bedroom AC Turn Off)","func":"msg.payload = { \"master_bedroom_ac\": { \"state\": \"off\" } };\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":240,"wires":[["f0d2632f67a083ad","78272c272af9bfa3"]]},{"id":"78272c272af9bfa3","type":"debug","z":"b329f38a.b28f9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":950,"y":120,"wires":[]},{"id":"6208f784bbd6b1d0","type":"debug","z":"b329f38a.b28f9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":390,"y":140,"wires":[]},{"id":"023f4b3f191f06a0","type":"switch","z":"b329f38a.b28f9","name":"","property":"payload.event.service_data.entity_id","propertyType":"msg","rules":[{"t":"eq","v":"scene.master_bedroom_ac_turn_on","vt":"str"},{"t":"eq","v":"scene.master_bedroom_ac_turn_off","vt":"str"},{"t":"eq","v":"scene.master_bedroom_ac_increase_temperature","vt":"str"},{"t":"eq","v":"scene.master_bedroom_ac_decrease_temperature","vt":"str"}],"checkall":"false","repair":false,"outputs":4,"x":210,"y":400,"wires":[["81427887493dcaae"],["22fff276e889ea6f"],["d4b62abb623a0a57"],["d4b62abb623a0a57"]]},{"id":"d4b62abb623a0a57","type":"file in","z":"b329f38a.b28f9","name":"","filename":"/data/custom_sensors/sensor.master_bedroom_ac.json","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":530,"y":400,"wires":[["400f6165aa5de09c"]]},{"id":"febddc72d1271abe","type":"debug","z":"b329f38a.b28f9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":930,"y":380,"wires":[]},{"id":"400f6165aa5de09c","type":"json","z":"b329f38a.b28f9","name":"","property":"payload","action":"","pretty":false,"x":830,"y":440,"wires":[["febddc72d1271abe","aea1ca234ab9684c","1ea94d4fe766302e"]]},{"id":"aea1ca234ab9684c","type":"function","z":"b329f38a.b28f9","name":"Transform Payload (Master Bedroom Increase Temperature)","func":"var currentState = msg.payload.master_bedroom_ac.state;\nvar currentTemperature = msg.payload.master_bedroom_ac.temperature;\n\nif (currentState === 'on') {\n if (currentTemperature < 25) {\n currentTemperature = currentTemperature + 1;\n }\n msg.payload = { \"master_bedroom_ac\": { \"state\": \"on\", \"temperature\": currentTemperature } };\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":280,"wires":[["f0d2632f67a083ad"]]},{"id":"1ea94d4fe766302e","type":"function","z":"b329f38a.b28f9","name":"Transform Payload (Master Bedroom Decrease Temperature)","func":"var currentState = msg.payload.master_bedroom_ac.state;\nvar currentTemperature = msg.payload.master_bedroom_ac.temperature;\n\nif (currentState === 'on') {\n if (currentTemperature > 18) {\n currentTemperature = currentTemperature - 1;\n }\n msg.payload = { \"master_bedroom_ac\": { \"state\": \"on\", \"temperature\": currentTemperature } };\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":320,"wires":[["f0d2632f67a083ad"]]},{"id":"461c4d2a.d4d3c4","type":"server","name":"Home Assistant","version":1,"legacy":false,"addon":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]
JSON sensor file:
{ "master_bedroom_ac" : { "state" : "on", "temperature" : 20 } }
Since both Home Assistant and Node-RED are in Docker, I needed a way to read the JSON file which was in Node-RED’s volume in Home Assistant. To accomplish this, I created a custom_sensors
folder in Home Assistant’s volume. I then created a bash script that watches the custom_sensors
folder in Node-RED’s volume for any changes and copies the changed files to Home Assistant’s volume.
#!/usr/bin/env bash
## The source and target can contain spaces as
## long as they are quoted.
source="/home/pi/Docker/Node-RED/data/custom_sensors"
target="/home/pi/Docker/Home Assistant/config/custom_sensors"
while true; do
## Watch for new files, the grep will return true if a file has
## been copied, modified or created.
inotifywatch -e modify -e create -e moved_to -t 1 "$source" 2>/dev/null |
grep total &&
## The -u option to cp causes it to only copy files
## that are newer in $source than in $target. Any files
## not present in $target will be copied.
sudo cp -vu "$source"/* "$target"/
# rsync -rc "$source/" "$target/" --delete
done
I added this script to Raspberry Pi’s rc.local
file so that it would run at every boot.
With the JSON file now appearing in Home Assistant’s volume, I proceeded to create File sensors in Home Assistant.
homeassistant:
whitelist_external_dirs:
- /config
- /config/custom_sensors
allowlist_external_dirs:
- /config/custom_sensors
- /config
sensor:
- platform: file
name: Master Bedroom AC State
file_path: /config/custom_sensors/sensor.master_bedroom_ac.json
value_template: '{{ value_json.master_bedroom_ac.state }}'
- platform: file
name: Master Bedroom AC Temperature
file_path: /config/custom_sensors/sensor.master_bedroom_ac.json
value_template: '{{ value_json.master_bedroom_ac.temperature }}'
unit_of_measurement: "°C"
Finally, I created a custom card to visualize and control the AC.
type: custom:stack-in-card
mode: vertical
cards:
- type: horizontal-stack
cards:
- type: custom:button-card
show_name: false
icon: mdi:thermometer-chevron-up
size: 50px
styles:
icon:
- color: |
[[[
if (states['sensor.master_bedroom_ac_state'].state === 'on')
return "green";
if (states['sensor.master_bedroom_ac_state'].state === 'off')
return "#44739e"
return "#bdbdbd";
]]]
- margin: 40px 0 0 0
tap_action:
action: call-service
service: scene.turn_on
service_data:
entity_id: scene.master_bedroom_ac_increase_temperature
- type: custom:button-card
show_name: true
name: '[[[ return states[''sensor.master_bedroom_ac_temperature''].state ]]]'
show_icon: true
icon: mdi:power-standby
size: 200px
show_label: true
label: Master Bedroom AC
styles:
name:
- color: green
- font-size: 100px
- display: |
[[[
if (states['sensor.master_bedroom_ac_state'].state === 'on') return `block`
return `none`
]]]
icon:
- color: '#44739e'
- display: |
[[[
if (states['sensor.master_bedroom_ac_state'].state === 'off') return `block`
return `none`
]]]
tap_action:
action: call-service
service: scene.turn_on
service_data:
entity_id: |
[[[
if (states['sensor.master_bedroom_ac_state'].state === 'on') return `scene.master_bedroom_ac_turn_off`
return `scene.master_bedroom_ac_turn_on`
]]]
- type: custom:button-card
show_name: false
icon: mdi:thermometer-chevron-down
size: 50px
styles:
icon:
- color: |
[[[
if (states['sensor.master_bedroom_ac_state'].state === 'on')
return "green";
if (states['sensor.master_bedroom_ac_state'].state === 'off')
return "#44739e"
return "#bdbdbd";
]]]
- margin: 40px 0 0 0
tap_action:
action: call-service
service: scene.turn_on
service_data:
entity_id: scene.master_bedroom_ac_decrease_temperature
There are still several issues with this approach:
- This only works with all flows starting and ending with Home Assistant. If a remote is used, there is no way the JSON would get updated.
- There is a delay in updating the sensor, which is anywhere from 1 - 8 seconds.
- Based on your air conditioner vendor, the remotes configured via the Smart Life app can issue different commands which makes it more cumbersome to use this workaround. For example, I have an LG air conditioner configured in the Smart Life app. Creating a scene to increase or decrease temperature results in scenes with the specific temperature being increased or decreased. Hence I’ll have to create multiple scenes for the increase and decrease commands.
In conclusion, the above workaround is very clunky and I’m going to start working on the Tasmota-IRHVAC and LG-Climate-component-for-Home-Assistant projects on GitHub and will take me a couple of months of hard work but it may be worth it.