Ac infinity controller 67 Bluetooth temp, humidity, fan pwm

Cheers, thank you very much!!

ill try to figure out what the code is for setting the min max for auto settings, sounds fun

better to set that value in home assistant and base your automation off the senor data.

high low temp value is found in any message from master to slave and when sent back even after notifying slave it doesnt respond to changing high low value of controller itself.

Yes I do have automations based on HA sensors, but HA sets its target humidity auto based on data while on AC I have to set it manually. Sometimes they conflict and fight each other, so HA will try to up the humidity and AC will be running on max setting to lower it.

Anyways, thanks for taking a look at it!

What Iā€™m trying to say is use the sensor data from the ac infinity probe in Lovelace then build automation based on that data

If I only want to control the fan do I just use the .sh file and add the code to the yaml config? I have my own sensors inside etc so Im just looking for fan control at the moment.

Would this also work with the other UIS ports? For example, I have 6 ACI clip on fans connected to UIS port 4 and would like to be able to control the speed of them.

Oh you mean using the data and automate around the BT script? If so, I planned to go that route at the beginning but two times out of three the bluetooth command would fail to execute. I even tried adding a few more checks to the script on the connection but it was still failing too many times. I prefer to not have it in HA but having the right speed at most times.

The script I was left at the end is this for what is worth:

#!/bin/bash

macaddress="XX:XX:XX:XX:XX:XX"

if [[ $1 == 0* ]]; then speed="0x00 0xaa 0x33"; fi
if [[ $1 == 1* ]]; then speed="0x01 0xba 0x12"; fi
if [[ $1 == 2* ]]; then speed="0x02 0x8a 0x71"; fi
if [[ $1 == 3* ]]; then speed="0x03 0x9a 0x50"; fi
if [[ $1 == 4* ]]; then speed="0x04 0xea 0xb7"; fi
if [[ $1 == 5* ]]; then speed="0x05 0xfa 0x96"; fi
if [[ $1 == 6* ]]; then speed="0x06 0xca 0xf5"; fi
if [[ $1 == 7* ]]; then speed="0x07 0xda 0xd4"; fi
if [[ $1 == 8* ]]; then speed="0x08 0x2b 0x3b"; fi
if [[ $1 == 9* ]]; then speed="0x09 0x3b 0x1a"; fi
if [[ $1 == 10* ]]; then speed="0x0a 0x0b 0x79"; fi

/usr/bin/expect <(cat <<'EOF'
set prompt "#"
set speed [lindex $argv 0]
set address [lindex $argv 1]
spawn bluetoothctl
expect "AdvertisementMonitor path registered\r"
send "scan on\r"
sleep 3
send "connect $address\r"
expect "Connection successful"
send "menu gatt\r"
send "select-attribute /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service001b/char001e\r"
send "notify on\r"
send "select-attribute /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/service001b/char001c\r"
send "write \"0xa5 0x00 0x00 0x06 0x00 0x67 0x02 0xd8 0x00 0x03 0x10 0x01 0x02 0x12 0x01 $speed\"\r"
send "back\r"
send "disconnect\r"
expect -re $prompt
send "quit\r"
expect eof
EOF
) "$speed" "$macaddress"

Iā€™m using an rpi400 (the keyboard one) which is BT5 and is about 4-5 meters from the AC unit so I donā€™t think range is an issue here.

Hey all,

Stumbled on this thread, looking for ways to work with my local tuya devices.

I am new here, but I have been successful at reading the details from the Bluetooth advertisement packets. I too went insane for probably a week trying to decode the encoding from reverse engineering the APK file code.

I was successful in utilizing a Bluetooth script and a raspberry pi to grab the data from this advertisement packets from my 67 and 69 controllers.

I am looking forward to integrating some of the controlling commands that you all have in this thread, but Iā€™m not sure if anyone here has been able to decode what I figured out. I will post this script snippet that is very simple to gather the details from the controllers and print them to the screen.

Itā€™s getting late here but please donā€™t pull your hair out if you have a 67 or 69 controller because Iā€™ve got you covered.

Will post my findings tomorrow. Itā€™s a single line to decode temp, humidity and fan speed. I currently utilize zabbix for graphing all of my data long term.

Cheers and get some sleep!

Okay, hereā€™s the details:

Requirements:

  • bluepy 1.30
  • bluez 5.4.7

Essentially, the temperature, humidity and fan speed is encoded in the scan packets (I call them advertisements) ā€¦ I found their encoding mechanisms in the code for the application, and reverse engineered the positions and values. For the longest time I thought the data came from a notification etcā€¦

Hereā€™s the single line that I worked on for week :frowning:

temp, hum, fan = (
                float(struct.unpack(">h", d[16:18])[0] / 100 * 9 / 5 + 32),
                float(struct.unpack(">h", d[18:20])[0] / 100),
                d[20],
            )

The demo script below essentially listens for BTLE scan packets for known devices, and in my configuration, when a device MAC is known, will emit monitoring metrics to my zabbix system for trending and alerting purposes. If you have any questions about getting this up and running please message me.

import struct
import binascii
from bluepy.btle import Scanner, DefaultDelegate
from time import sleep

LEAF_OFFSET = -3.8

def get_name(device):
    if device == "00:00:00:00:00:00":
        return "room1"
    elif device == "00:00:00:00:00:00":
        return "room2"

def get_vpd(humidity: float, temperature: float, leaf_offset: float = 0.0):
    # VPD Calculation
    temp_ambient = (temperature - 32) / 1.8
    # Leaf surface temperature is typically -3 to -4 with LED
    # We substract -35 instead of -32 for the normal F->C calc
    temp_leaf = (temperature - (32 - leaf_offset)) / 1.8
    vpsat = 610.7 * 10 ** ((7.5 * temp_ambient) / (237.3 + temp_ambient)) / 1000
    vpsat_leaf = 610.7 * 10 ** ((7.5 * (temp_leaf)) / (237.3 + (temp_leaf))) / 1000
    # This is the vpair calculation. If we don't want to use leaf
    # surface, we could use this equation: vpd = round(vpsat - vpair,2)
    # vpair = (610.7*10**((7.5*temperature_c)/(237.3+temperature_c))/1000*(h/100))
    vpd_result = round(vpsat_leaf - (vpsat * humidity / 100), 2)
    return vpd_result


class ScanDelegate(DefaultDelegate):
    def __init__(self):
        DefaultDelegate.__init__(self)

    def handleDiscovery(self, dev, isNewDev, isNewData):
        device_list = [
            "00:00:00:00:00:00",
        ]
        
        addr = dev.addr

        if isNewDev:
            print(f"Discovered device: {addr}")
        
        elif isNewData and addr in device_list:
            name = get_name(addr)
            print(f"Received new data from {addr}")
            scan_data = dev.getScanData()[2][2]
            d = binascii.unhexlify(scan_data)
            temp, hum, fan = (
                float(struct.unpack(">h", d[16:18])[0] / 100 * 9 / 5 + 32),
                float(struct.unpack(">h", d[18:20])[0] / 100),
                d[20],
            )
            vpd = get_vpd(humidity=hum, temperature=temp, leaf_offset=LEAF_OFFSET)
            print(f"Device name: {name} - {addr} - Temp: {temp} - Humidity: {hum} - Fan: {fan}")
            if temp > 0 and hum > 0 and fan > -1 and vpd > 0: # if all values are valid
                ## do something with the metrics here
                pass

try:
    scanner = Scanner().withDelegate(ScanDelegate())
    scanner.scan(15.0)
    sleep(25)
    scanner.scan(15.0)
except Exception as e:
    print(e)

The code is a bit messy and not how Iā€™d generally prefer to see it, but I put it all in one file for this example.

Essentially, we start a scan for 15 seconds, wait 25 seconds, and scan for another 15 seconds. This assures I pick up at least 2-3 samples per minute for my needs. You can play around with the scan run time to make sure you capture all the advertisements for your controller(s). One other side note: When you connect with the app, it seems to setup a two-way communication with the controller and the scan packets are no longer emitted. Just keep that in mind if you happen to leave the app open and see missing data. In zabbix I just setup a missing data check to assure I am always receiving data.

Hereā€™s an example of what the output should look like from the demo script above:

I hope this helps someone out there. I went through a lot of trial and error, and a lot of traffic sniffing. Ultimately I had to compare the encoding used in their application and reverse engineer using unhexlify and struct :+1:

Have a great day everyone.

3 Likes

Awesome! Maybe someone can get this into MQTT next!

1 Like

I had to dig through the APk as well to figure out how they supplied the monitor data. You can see my code to grab it above. Definitely not as well written as what youā€™ve done! Very nice work!

Mine just works on home assistant and use a string to decipher it in config yaml

1 Like

Appreciate everyoneā€™s involvement including @Neonerd5 , @nmiguez thanks all!

1 Like

I noticed that youā€™ve got an error in .sh file for temp/humidity - you try to ā€œlist-attributesā€ before going into the gatt menu. When I tried to place it after the ā€œmenu gattā€, it produces a different output than if itā€™s in the incorrect place. Then when the script is called to obtain the temperature, it fails becuse the xargs printf at the end tries to parse invalid data.

If I keep the ā€˜list-attributesā€™ in the original location as per your script, the resulting output from the ā€˜Temp in Cā€™ execution is a strange value - 2746 in my case. The ā€œTemp in Fā€ fares a little better and returns 81, which is probably about right. The humidity command returns 2494, which is similar to the one returned in the Temp in C command.

Iā€™ll need to re-read this thread to work out if these large values need to be converted to something human readable - or perhaps the script isnā€™t working correctly for me?

What an interesting rabbit hole Iā€™m burrowing at the moment. Iā€™ve taken the example code displayed here over the last page or two, given it to chatgpt, and asked it to make changes. I wanted it to change the python code to write a log file of the temp, humidity, and fan speed that it obtains from the bluetooth polling.
It produces perfect bash script code and python code. I then asked it to write yaml so that Home Assistant can read the log file created and display it as a time-series graph. It then generated the code.

I donā€™t yet have any experience with HA or how this should all work, but at least itā€™s not pretty much possible to have chatgpt write whatever code I need it to make. It seems to know about HA and of course python, bash, etc.

If youā€™re curious, hereā€™s what I asked it: ā€œwrite yaml that will read the file that was created by the python script, extract the temp, and display it as a time graphā€. It produced this:

ā€œTo read a file, extract temperature data, and display it as a time graph in Home Assistant using YAML configuration, you can use the File Sensor and the History Graph components. Hereā€™s an example YAML configuration:ā€


sensor:
  - platform: file
    name: temperature_data
    file_path: /path/to/theatre_climate_history.log
    value_template: "{{ value.split(' - ')[1].split(':')[1].strip() }}"
    unit_of_measurement: Ā°F

history_graph:
  temperature_history:
    entities:
      - sensor.temperature_data
    hours_to_show: 24
    refresh: 60

In this example:

1. The sensor section configures the File Sensor to read the file theatre_climate_history.log located at /path/to/theatre_climate_history.log. Adjust the file_path according to the actual path of your log file. The value_template extracts the temperature data from each line of the file. Adjust the template if the log file format differs. The unit_of_measurement specifies the unit of measurement for the temperature data.
2. The history_graph section creates a history graph named temperature_history. It includes the sensor.temperature_data entity, which represents the temperature data read from the file. Adjust the entity name if necessary. The hours_to_show defines the duration of the graph, and refresh specifies the refresh rate in seconds.

Save the YAML configuration to your Home Assistant configuration file, then restart Home Assistant for the changes to take effect. Afterward, you should be able to see the temperature data displayed as a time graph in the Home Assistant interface.

Note: Ensure that you have the necessary components (sensor and history_graph) enabled and configured in your Home Assistant installation.

And hereā€™s the modified python code it created based on my providing Neonerd5ā€™s code:

import struct
import binascii
from bluepy.btle import Scanner, DefaultDelegate
from time import sleep
from datetime import datetime

LEAF_OFFSET = -3.8

def get_name(device):
    if device == "xx:xx:xx:xx:xx:xx": # change to the lowercase mac address of your fan
        return "Theatre" # change to the name of the room it's in

def get_vpd(humidity: float, temperature: float, leaf_offset: float = 0.0):
    # VPD Calculation
    temp_ambient = (temperature - 32) / 1.8
    # Leaf surface temperature is typically -3 to -4 with LED
    # We subtract -35 instead of -32 for the normal F->C calc
    temp_leaf = (temperature - (32 - leaf_offset)) / 1.8
    vpsat = 610.7 * 10 ** ((7.5 * temp_ambient) / (237.3 + temp_ambient)) / 1000
    vpsat_leaf = 610.7 * 10 ** ((7.5 * (temp_leaf)) / (237.3 + (temp_leaf))) / 1000
    # This is the vpair calculation. If we don't want to use leaf
    # surface, we could use this equation: vpd = round(vpsat - vpair,2)
    # vpair = (610.7*10**((7.5*temperature_c)/(237.3+temperature_c))/1000*(h/100))
    vpd_result = round(vpsat_leaf - (vpsat * humidity / 100), 2)
    return vpd_result


class ScanDelegate(DefaultDelegate):
    def __init__(self):
        DefaultDelegate.__init__(self)

    def handleDiscovery(self, dev, isNewDev, isNewData):
        device_list = [
            "xx:xx:xx:xx:xx:xx" #change to the same mac address you entered earlier, in lowercase
        ]

        addr = dev.addr

        if isNewData and addr in device_list:
            name = get_name(addr)
            scan_data = dev.getScanData()[2][2]
           if len(scan_data) >= 20:
                d = binascii.unhexlify(scan_data)
                temp, hum, fan = (
                    float(struct.unpack(">h", d[16:18])[0] / 100 * 9 / 5 + 32),
                    float(struct.unpack(">h", d[18:20])[0] / 100),
                    d[20],
                )
                vpd = get_vpd(humidity=hum, temperature=temp, leaf_offset=LEAF_OFFSET)
                print(f"Device name: {name} - {addr} - Temp: {temp} - Humidity: {hum} - Fan: {fan}")
                if temp > 0 and hum > 0 and fan > -1 and vpd > 0: # if all values are valid
                    ## write out each record to a file, appending it each time.  Include a timestamp.
                    with open("climate_history.log", "a") as file:
                        # Get the current date and time
                        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                        # Write the data with the date stamp
                        file.write(f"{timestamp} - Temp: {temp} - Humidity: {hum} - Fan: {fan}\n")
                    pass

try:
    scanner = Scanner().withDelegate(ScanDelegate())
    scanner.scan(15.0)
    sleep(25)
    scanner.scan(15.0)
except Exception as e:
    print(e)

Until I understand how HA needs the output data in order to display it, I canā€™t know what code is required. As for being able to change fan speed, Iā€™ve tried the bash code in this thread and it works. Once I know what HA needs in order to display a control for it, Iā€™ll ask chatGPT to generate the code.

Update several hours later: I (well, ChatGPT and me) wrote a python script to find the AC-Infinity bluetooth controller, retrieve, and display the fan speed info. Itā€™s not much different than code already posted in here so I wonā€™t bother pasting it. Iā€™m currently trying to figure out how to write widget thingies so that I can integrate it into HA but thatā€™s a new rabbit hole.

2 Likes

Youā€™re very smart. I canā€™t even find the Bluetooth MAC address. I read somewhere on the HA instructions for the 69 controller that I needed to use gatttool, but when I run
apk add gatttool
I get a ā€œno such packageā€
error

Whoa thatā€™s amazing! Quick q, I have speed controller with the UIS connector. I don;t have the controller 67, just the button. Do you know if I would be able to control the fan from HA? Ideally I would want to connect it to an esp32 board to I could control it via mqtt. Do you have any insight on that?

It appears to use bluetooth, in which case it almost certainly uses the same protocols and commands as the 67. Some values may be different and thereā€™s like far more additional controls and commands that it uses since it appears to be a new architecture for numerous devices.

An update since yesterday. ChatGPT (actually itā€™s all me. I wrote it all. Iā€™m a programming god ok it was completely AI goddamit) has written several python scripts that let me play around with the 67 controller, but I was still blind about what values to change, where. So I started looking at the decompiled apk that was posted on git but found it too hard to navigate and find things. So I decompiled the apk myself
./jadx --deobf ~/acinfinity/AC\ Infinity_1.5.8_apk-dl.com.apk
then opened it in Android Studio Flamingo (in Ubuntu). I found the following interesting values that Iā€™ll likely start focusing in on. My intent is to work out the same sequence of commands, using the same values that the AC Infinity app does, to read and control the controller. Some of the sample code posted in here may or may not work because it usually writes values to UUIDs that, although they appear to work, may corrupt or change other values unintentionally. They also wonā€™t have correct CRC values so the device may ignore them. All this will likely mean that any sort of control from within Home Assistant will be flaky, intermittent, cause device hangs, or may not work on all devices.

Hereā€™s some tantalizing tidbits:

sources/com/eternal/base/global/BluetoothKey.java

pretty much more answers than I can digest at the moment.

Here are all the UUIDs used and their purpose:

FIRMWAREVERSION_UUID = "00002A26"
HARDWAREVERSION_UUID = "00002A27"
MAC_UUID = "00002A23-0000-1000-8000-00805F9B34FB"
MODEL_NAME_UUID = "00002A24-0000-1000-8000-00805F9B34FB"
SN_UUID = "00002A25-0000-1000-8000-00805F9B34FB"
SOFTWAREVERSION_UUID = "00002A28"
UUID_NOTIFY = "70D51002-2C7F-4E75-AE8A-D758951CE4E0"
UUID_NOTIFY_ESP = "0000ff02-0000-1000-8000-00805f9b34fb"
UUID_SERVICE = "70D51000-2C7F-4E75-AE8A-D758951CE4E0"
UUID_SERVICE_ESP = "0000ffff-0000-1000-8000-00805f9b34fb"
UUID_WRITE = "70D51001-2C7F-4E75-AE8A-D758951CE4E0"
UUID_WRITE_ESP = "0000ff01-0000-1000-8000-00805f9b34fb"

Hereā€™s some interesting values used for writes:

DEVICE_VPD_LEAF_TEMPERATURE_OFFSET = 37
DEVICE_TIME_REMAINING_BY_CURRENT_MODE = 24
DEVICE_SET_RESET_DEVICE_TO_FACTORY = 61
DEVICE_BACKLIGHT_BRINGHTNESS_GEAR = 33
DEFAULT_MTU = 247
DEVICE_CURRENT_OPERATING_MODE = 16
DEVICE_CURRENT_TEMP_HUM_STATUS = 2
DEVICE_FAN_GEAR_BY_OFF = 17
DEVICE_FAN_GEAR_BY_ON = 18
DEVICE_INFO = 0

Thereā€™s about 100 in all.

1 Like

Mine still works I donā€™t do updates tho so never needed to change anything sorry