18 Oct 2024

Fetch metrics from CTC Eco Zenith 255L using the mirror display web server

Suddenly an air/water heater model CTC Eco Zenith 255L and Eco Air 610M appeared. This thing is of course clown..err cloud ready. They seemed to have done a pretty decent job judging all the granular settings regarding internet connectivity and options for remote permissions.

However, having experienced the Mitsubishi Melclown clowpocalypse in April 2024 which rendered the air conditioner incapable of remote management while Mitsubishi kept frothing and DDosing their own servers for days with zero or less public acknowledgement. The physical remote control is next to useless…which is funny because its large as a brick but yet requires byzantine menu wrestling to achieve anything else than on/off. It will drive you immediately insane. so…clown connecting my uncontaminated CTC is a NO GO. Fine, I’ll just pull the local API.

Feck, there is no API. Documentation is pretty vague and refers to some non-existent mobile app for mirroring the display. So lets try to figure out of how this sofisticated mobile app communicates to the heat pump control unit (255L) without finding any sort of trace of this app in the wild.

Luckily, the control unit only have ethernet support by default, so after turning the whole system offline, the front plate has to be removed and an interwebz cable has to plugged into the back side of the display panel.

After setting up yet another segmented Internet of shit Zone for this appliance, it fetched IP from my DHCP. I enabled the web server in the settings pane using the touch screen. Port scan only revealed port 80 and browsing to the page just gives you “Bad request”. After some web cawling, it turns out you have to go to http:///main.htm So, here you are presented with a mirror of the touch panel, just like expected. It turns out to be some sort of pixel-mapped javascript abomination. Rotating the screen on a phone displaces the clickable area for the buttons, so you need to refresh the page.

After poking around with dev tools open in the browser, I saw it pulled data from URL:s like: http:///vars/glob;1;0;116 And the returned data was not very colorful:

4294967295|0|0|0|0|0|0|0|0|0|0|0|0|0|2|-1|-1|-28|4294967295|0|0|-20000|-28|4294967295|0|

I spent many hours to iterate through a few panes that I wanted to pull data from by trying to match whatever was changing in the pipe delimited hairball it returned after I changed values in the heater etc. Lots of the values seemed to have no real observable purpose.

I did sift through the firwmare binary avaiible on https://ctc.se just to see if it gave any hints, but could not find much more than javascript, and more javascript…and some certificates. Pretty unreadable javascript. Some API endpoints that I haven’t encountered in the menu system was exposed, such as /sm/All, but had no obvious indication of what the actual function was. The firmware gave away that this heater seems to run RTOS.

My vision was to pull the data using telegraf, then send the data into a PostgreSQL db with timescaledb extension and visualize using Grafana. Due to the nature of this unforgiving brutalist webdesign, I figured the easiest route to embody flawless success was to orchestrate this using telegrafs input exec plugin along with a few shell scripts.

Here are three scripts that fetches the data from the interesting panes. I mapped out the values when I found something useful. Like some weird coincidence, I wrote this script in ksh so it could run exquisitely on my RPI 4 with OpenBSD without installing any shell from ports.

control_unit.sh

#!/bin/ksh
api_endpoint="http://z.z.z.z"
api_control_unit=$(curl -s "${api_endpoint}/vars/glob;1;0;116" | tr -d '\r'| tr  '|' '\n')
################################
# Control Unit                 #
################################
set -A values $api_control_unit
epoch=$(date -j -f "%Y %m %d %H %M" "${values[1]} ${values[2]} ${values[3]} ${values[4]} ${values[5]} ${values[6]}" +%s)
json="{\"control_unit\":[{\"timestamp\":$epoch},"
i=0
while [ $i -lt ${#values[@]} ]; do
        case $i in
                52)
                        name="\"Tank Lower (Current)\""
                        ;;
                51)
                        name="\"Tank Lower (Set Point)\""
                        ;;
                50)
                        name="\"Tank upper (Set Point)\""
                        ;;
                49)
                        name="\"Tank Upper (Current)\""
                        ;;
                48)
                        name="\"Status\""
                        ;;
                *)
                        name=\"$i\"
        esac

        json="$json{$name:${values[$i]}},"
        i=$((i + 1))
done

json="${json%,}]}"
echo $json | jq '.'

heating_circuit.sh

#!/bin/ksh
api_endpoint="http://z.z.z.z"
api_heating_circuit_1=$(curl -s "${api_endpoint}/vars/glob;1;0;6;115" | tr -d '\r'| tr  '|' '\n')
################################
# Heating Circuit 1            #
################################

set -A values $api_heating_circuit_1
epoch=$(date -j -f "%Y %m %d %H %M" "${values[1]} ${values[2]} ${values[3]} ${values[4]} ${values[5]} ${values[6]}" +%s)
json="{\"heating_circuit\":[{\"timestamp\":$epoch},"
alength=${#values[@]}
i=0
while [ $i -lt ${#values[@]} ]; do
        name=$i
        if [ $i -eq  $((alength - 1  )) ]; then
                json="$json{\"$name\":${values[$i]}} ]}"

        else
                json="$json{\"$name\":${values[$i]}},"
        fi
        i=$((i + 1))

done
echo $json|jq '.'

operation_hp.sh

#!/bin/ksh
api_endpoint="http://z.z.z.z"
api_operation_hp=$(curl -s "${api_endpoint}/vars/glob;1;0;117" | tr -d '\r'| tr  '|' '\n')
################################
# Operation HP                 #
################################

set -A values $api_operation_hp
epoch=$(date -j -f "%Y %m %d %H %M" "${values[1]} ${values[2]} ${values[3]} ${values[4]} ${values[5]} ${values[6]}" +%s)
json="{\"Operation_HP\":[{\"timestamp\":$epoch},"
i=0
while [ $i -lt ${#values[@]} ]; do
        case $i in
                63)
                        name="\"Status\""

                        if [ ${values[$i]} -eq 14 ]; then
                                values[$i]="false"
                        elif [ ${values[$i]} -eq 28 ]; then
                                values[$i]="true"
                        fi
                        ;;
                64)

                        name="\"Compressor RPS\""
                        if [ ${values[$i]} -eq 200 ]; then
                                values[$i]=0
                        fi
                        ;;
                67)     name="\"Charge Pump\""
                        ;;
                68)
                        name="\"Charge Pump Percentence\""
                        ;;
                76)
                        name="\"Outdoor temp\""
                        ;;

                67)
                        if [ ${values[$i]} -eq 1 ]; then
                                values[$i]=true
                        elif [ ${values[$i]} -eq 0 ]; then
                                values[$i]="false"
                        fi
                        name="\"Fan\""
                        ;;

                *)
                        name=\"$i\"
        esac

        json="$json{$name:${values[$i]}},"
        i=$((i + 1))
done
json="${json%,}]}"
echo $json | jq '.'

Enjoy the murky touch by the tendrils of forbidden knowledge.