Ac infinity controller 67 Bluetooth temp, humidity, fan pwm

because the commands are there and it looks cool? it may save some electrical cost? power consumption

honestly no, in mine i have it set the same way expect i added off at 10 setting, just because i started with on10 / off 10

ive been playing with 67 for a hour or so and theres nothing coming from the thing in terms of temp/humid readings , which i find weird since ive found it on the 69 pro and the multi plug outlet

both using same uuid and just opened notifications.

doing the same on the 67 has just produced a silent uuid, so i sent commands to the other to get notifications and nothing promising so far

1 Like

notified from 70d51002-2c7f-4e75-ae8a-d758951c34e0

69 pro
1e ff 02 09 03 1c 4c 01 08 41 13 3b 00 * 7e * 00 00 00 05 1d 4c 00 01 ff ff 00 01 ff ff 00 01 ff ff 00 01

75 outlet
1e ff 02 09 03 14 4c 01 0a 27 0c 22 00 *d9 *3d b8 00 01 3d b8 00 03 3d b8 00 01

on the 67 im getting no response, even when poking it with a few values

@jayrama ive download the apk to pc, ill see if i can take a look inside and see if theres anything useful

 private static void resoleCDevice(Device device, byte[] bArr) {
        byte b = bArr[13];
        device.isDegree = !ByteUtils.getBit(b, 1);
        device.tmpState = (byte) ByteUtils.getBits(b, 4, 2);
        device.humState = (byte) ByteUtils.getBits(b, 6, 2);
        device.tmp = ByteUtils.getShort(bArr, 15) * 10;
        device.hum = (bArr[17] & 255) * DataFragment.TAG_FILTER_TEMPERATURE;
        byte b2 = bArr[14];
        device.autoHighTmpSwitch = !ByteUtils.getBit(b2, 4);
        device.autoLowTmpSwitch = !ByteUtils.getBit(b2, 5);
        device.autoHighHumSwitch = !ByteUtils.getBit(b2, 6);
        device.autoLowHumSwitch = !ByteUtils.getBit(b2, 7);
        if (device.isDegree) {
            device.autoHighTmp = bArr[20];
            device.autoLowTmp = bArr[21];
        } else {
            device.autoHighTmp = bArr[18];
            device.autoLowTmp = bArr[19];
        }
        device.autoHighHum = bArr[22];
        device.autoLowHum = bArr[23];
    }

    private static void resoleABDevice(Device device, byte[] bArr) {
        byte b = bArr[13];
        device.isDegree = true ^ ByteUtils.getBit(b, 1);
        device.fanState = (byte) ByteUtils.getBits(b, 2, 2);
        device.tmpState = (byte) ByteUtils.getBits(b, 4, 2);
        device.humState = (byte) ByteUtils.getBits(b, 6, 2);
        device.tmp = ByteUtils.getShort(bArr, 14);
        device.hum = ByteUtils.getShort(bArr, 16);
        device.fan = bArr[18];
    }
}
06:26:11.971 - Connecting to nearby peripheral: 4GRDH 
06:26:12.537 - Connected to nearby peripheral: 4GRDH 
06:26:12.638 - Discovered nearby peripheral: (null) (RSSI: -86) 
06:26:13.522 - Characteristic (2A24) read: <41432049 6e66696e 69747900> 
06:26:13.545 - Characteristic (2A27) read: <312e31> 
06:26:13.575 - Characteristic (2A28) read: <312e302e 3534> 
06:26:13.651 - Discovered nearby peripheral: M203T_d16b74 (RSSI: -71) 
06:26:13.872 - Stopping search for nearby peripherals 
06:26:16.342 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) read: <11223344> 
06:26:18.233 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08070e7c 00930000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:47.232 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08040e42 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:48.237 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e44 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:49.235 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e45 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:50.233 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08040e44 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:51.230 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08030e42 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:52.235 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e40 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:53.232 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e44 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:54.230 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e44 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:55.250 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08030e3c 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:56.233 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e3f 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 
06:37:57.230 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <1eff0209 031c0001 08060e49 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001> 


<1eff0209 031c0001 08060e49 00950000 00051d4c 0001ffff 0001ffff 0001ffff 0001>

think temp humidity are located here

apk is on another pc, i used @luma github repo to pull these

1 Like
public static int getTmp(int i, boolean z) {
        if (i == -32768) {
            return 0;
        }
        if (z) {
            return Math.round(((float) i) / 100.0f);
        }
        return Math.round((((((float) i) / 100.0f) * 9.0f) / 5.0f) + 32.0f);

DUDE. GET ME ON YOUR TEST. I have my security camera running a timelapse doing a snapshoit every 15 minutes with Blue Iris.

I’m crop steering, I’ve got all the bells and whistles to measure moisture, EC etc in the root zone.

my test are usually clunky things that work together

so far i just have to upload a base64 image to something crazy Jay would figure it out in seconds watch

Clunky things that work together is literally my life.

1 Like

awesome, will take a look at the sensor stuff later today, thanks mikey.

new project sounds interesting, im using frigate with a google coral here for camera AI

1 Like

so…pulled new data from the 69 to finish integration, from how to reverse bluetooth device on youtube…

and finding the checksum of the value has killing me for awhile… but i think i found something

dunno if its something or not but here goes

CRC-16/CCITT-FALSE

by removing the front common and incrementing value were left with “00031001 021201 0aff01 209a”

putting that into crccalc and removing the end byte 20 9a, it returns CRC-16/CCITT-FALSE to have value 209a

works on all on commands so far, 1 step closer?

a5000008 00ac71fe 00031001 021201 0aff01 209a
a5000008 00b5f2e6 00031001 021201 09ff01 79ca
a5000008 00cc1d58 00031001 021201 08ff01 4efa
a5000008 00db7f8e 00031001 021201 07ff01 62cb
a5000008 00ea59fc 00031001 021201 06ff01 55fb
a5000008 00f97bae 00031001 021201 05ff01 0cab
a5000008 0106566f 00031001 021201 04ff01 3b9b
a5000008 0117547f 00031001 021201 03ff01 be0b
a5000008 012322a8 00031001 021201 02ff01 893b
a5000008 013110db 00031001 021201 01ff01 d06b
a5000008 013bb191 00031001 021201 00ff01 e75b

a5000008 003b82a0 00031001 011101 0aff01 00a8
a5000008 0049dc75 00031001 011101 09ff01 59f8
a5000008 00550fc8 00031001 011101 08ff01 6ec8
a5000008 006719d9 00031001 011101 07ff01 42f9
a5000008 00725b4d 00031001 011101 06ff01 75c9
a5000008 007f8ae0 00031001 011101 05ff01 2c99
a5000008 008a355a 00031001 011101 04ff01 1ba9
a5000008 0096e6e7 00031001 011101 03ff01 9e39
a5000008 00ac71fe 00031001 011101 02ff01 a909
a5000008 00b8234b 00031001 011101 01ff01 f059
a5000008 00d84fed 00031001 011101 00ff01 c769
import asyncio
from bleak import BleakClient
from crccheck.crc import CRC-16/CCITT-FALSE

address = "34:85:18:6a:52:52"

async def main(address):
    async with BleakClient(address) as client:
        header = bytes.fromhex("a500000800ac71fe")
        command = bytes.fromhex("000310010212010aff01")
        crcinst = CRC-16/CCITT-FALSE
        crcinst.process(command)
        model_number = await client.write_gatt_char("70d510012c7f4e75ae8ad758951ce4e0 ", header + command + crcinst.finalbytes())

asyncio.run(main(address))

remember pip install asyncio bleak crccheck

1 Like

too bad crccheck.crc doesnt include crc-16/CITT…

you already do this?

Wow, amazing. you folks are kicking ass!
Abou the the WIFI BLu Pro model controller, at the beginning of a new setup after connecting to the controller by bluetooth, app asks if you want to use WIFI or bluetooth to connect.

Perhaps need to remove the controller on the app, and add it back but use WIFI as your selection, then it uses wifi and not Bluetooth. This allows you to update firmware and connect from anywhere.

I suppose bluetooth is used by all controllers so it makes sense to use that. I am new to HA, and only learned smartthings “API” if you will they ended a couple of years ago.

I have a huge learning curve to even try to get this working. my mobo has bluetooth. I have HA installed, discovered a few devices and stopped due to time. any advice is greatly appreciated.

I am interested in using the MLX90614 as well, but obviously, that is another hurdle.

your right… temp is sent in the message in kelvin, they had it posted right in the code…

public static float getFah(float f) {
return ((float) Math.round((((f * 9.0f) / 5.0f) + 32.0f) * 100.0f)) / 100.0f;

this lead me to figuring out:::
66F 39%rh
1eff0209031c0201076a0f63007d000000051d4c0001ffff0001ffff0001ffff0001

Temp = 076a = 1898
temp = f
1898 * 9 / 5 ) + 3200) / 100 = 66.164

Humidity = 0f63 = 3939 move the decimal point 39.39% rh

VPD = 007d = 125 move decimal point 1.25 kPa

i prefer the bluetooth over wifi, easier to sniff atleast for me

have found humidity on the 67 by sending

A500000302e17e04000102032078e9 to uuid 70d510012c7f4e75ae8ad758951ce4e0

while listening for notifications on uuid 70d510022c7f4e75ae8ad758951ce4e0

value returned is a510001102e1575d000102050a0c4d1130030500000000002001009fe2

a510001102e1575d000102050a 0c4d 1130 030500000000002001009fe2

0c4d is Temperature
1130 is humidity

09:57:43.763 - Characteristic (70D51001-2C7F-4E75-AE8A-D758951CE4E0) wrote new value: <a5000003 034979d7 00010203 2078e9> 
09:57:43.815 - Characteristic (70D51001-2C7F-4E75-AE8A-D758951CE4E0) read: (null) 
09:57:43.816 - Characteristic (70D51002-2C7F-4E75-AE8A-D758951CE4E0) notified: <a5100011 0349508e 00010205 0007bd0f 3c030500 00000000 20010093 15> 

GAME SET MATCH

youll have to do the same notification then send command but after that, go back and read the notification uuid to retrieve info…

THIS WORKS

#!/usr/bin/expect -f

set prompt "#"
set address f0:74:59:49:c0:45
spawn bluetoothctl
expect -re $prompt
send "connect $address\r"
expect "Connection successful"
send "list-attributes\r"
send "menu gatt\r"
send "select-attribute /org/bluez/hci0/dev_F0_74_59_49_C0_45/service001b/char001e\r"
send "notify on\r"
send "select-attribute /org/bluez/hci0/dev_F0_74_59_49_C0_45/service001b/char001c\r"
send "write \"0xa5 0x00 0x00 0x03 0x03 0x49 0x79 0xd7 0x00 0x01 0x02 0x03 0x20 0x78 0xe9\"\r" 
send "select-attribute /org/bluez/hci0/dev_F0_74_59_49_C0_45/service001b/char001e\r"
send "read\r"
sleep 2
send "quit\r"
expect eof

Temp in C

/config/./FILENAME.sh | grep a5 | awk 'NR==2 { print "0x" $16 $17 }' | xargs printf "%d\n"

this will show temp in F from terminal

/config/./FILENAME.sh | grep a5 | awk 'NR==2 { print "0x" $16 $17 }' | xargs printf "%d\n" |xargs -n 1 bash -c 'echo $(($1 * 9/5 + 3200 ))' args | xargs -n 1 bash -c 'echo $(($1 /100))' args

Humidity

/config/./FILENAME.sh | grep '20 01\| a5' | awk 'NR==1 {print "0x" $18};NR==2 {print $3}' | awk 'NR%2{printf "%s",$0;next;}1' | xargs printf "%d\n"

call these proof of concept strings… if we can make it work from terminal we can make it work from lovelace

This is accurate. It reverts on update. I updated to the Feb update and the script stopped working. I re-ran your docker exec line thing and it works again immediately.

1 Like

trying to follow this reverse engineering ble devices guide and so far have this, not working code…

the crc is correct now, i made sure it only calculating the command and param portion to get correct checksum at the end…

i know we also need to notify the 70d510022c7f4e75ae8ad758951ce4e0 uuid but … i may have to get it working on the 69 and come back to the harder 67

import asyncio
from bleak import BleakClient
from crccheck.crc import Crc16CcittFalse

address = " f0:74:59:49:c0:45"

async def main(address):
    async with BleakClient(address) as client:

        header = bytes.fromhex("a5000006011dee34")
        command = bytes.fromhex("00")
        params = bytes.fromhex("310010212010")
        crcinst = Crc16CcittFalse
        crcinst.process(command)
        crcinst.process(params)
        model_number = await client.write_gatt_char("70d51001-2c7f-4e75-ae8a-d758951ce4e0", header + command + params + crcinst.finalbytes())

asyncio.run(main(address))

alright so the crazy part is if i break apart the header and params theres 2 checksums on this code…

a5000006011d checksum → ee34
000310010212010 checksum → 0b79

so my question is do we then add it something like header + crcinst.finalbytes() + command + params + crcinst2?.finalbytes())

how would that work?

10 on -     a5000006011d  ee34       000310010212010a  0b79
9 on-       a5000006013a  bab1       0003100102120109  3b1a
8 on -      a50000060146  05aa       0003100102120108  2b3b
7 on-       a50000060151  677c       0003100102120107  dad4
6 on-       a5000006015d  a6f0       0003100102120106  caf5
5 on -      a50000060169  d027       0003100102120105  fa96
4 on -      a50000060176  33f9       0003100102120104  eab7
3 on -      a50000060184  fca4       0003100102120103  9a50
2 on -      a50000060190  ae11       0003100102120102  8a71
1 on -      a5000006019b  1f7a       0003100102120101  ba12
0 on -      a500000601a8  194a       0003100102120100  aa33

something like ?>

import asyncio
from bleak import BleakClient
from crccheck.crc import Crc16CcittFalse

address = " f0:74:59:49:c0:45"

async def main(address):
    async with BleakClient(address) as client:

        header = bytes.fromhex("a5000006011d")
        command = bytes.fromhex("00")
        params = bytes.fromhex("03100102120100")
        crcinst = Crc16CcittFalse
        crcinst2 = Crc16CcittFalse 
        crcinst.process(header)
        crcinst2.process(command)
        crcinst2.process(params)
        model_number = await client.write_gatt_char("70d51001-2c7f-4e75-ae8a-d758951ce4e0", header + crcinst.finalbytes() + command + params + crcinst2.finalbytes())

asyncio.run(main(address))

working python code for 69… ill come back to 67 unless someone else figures out last bit on their own…

was able to get 69 to turn on with

import asyncio
from bleak import *
from bleak import BleakClient
from crccheck.crc import Crc16CcittFalse
address = "34:85:18:6a:52:52"

async def main(address):
    async with BleakClient(address) as client:

        header = bytes.fromhex("a5000008013bb191")
        command = bytes.fromhex("00")
        params = bytes.fromhex("0310010212010aff01209a")
        crcinst = Crc16CcittFalse()
        crcinst.process(header)
        crcinst.process(command)
        crcinst.process(params)
        model_number = await client.write_gatt_char("70d51001-2c7f-4e75-ae8a-d758951ce4e0", header + command + params + crcinst.finalbytes())

asyncio.run(main(address))

with this one it allows open command line , progress? maybe lots of errors which slows this code down now (fixed errors, moving onto seeing if i can create a full working code to integrate into home assistant using this as the starting point. By only needing to change single value (command) we can control the speed of the fan.

we can

import asyncio
from bleak import *
from bleak import BleakClient
from crccheck.crc import Crc16CcittFalse

address = "34:85:18:6a:52:52"

async def main(address):
    async with BleakClient(address) as client:

        aciuni = bytes.fromhex("a5000008013bb191")
        header = bytes.fromhex("00031001021201")
        command = bytes.fromhex("00") 
        params = bytes.fromhex("ff01")
        
        crcinst = Crc16CcittFalse()
        crcinst.process(header)
        crcinst.process(command)
        crcinst.process(params)
        model_number = await client.write_gatt_char("70d51001-2c7f-4e75-ae8a-d758951ce4e0",aciuni + header + command + params + crcinst.finalbytes())

asyncio.run(main(address))

heres what i have so far

init.py

"""ACI Universal COntroller"""

ACI_UNI.py

import asyncio
from bleak import BleakClient, BleakScanner
from crccheck.crc import Crc16CcittFalse
import logging

WRITE_UUID = "70d51001-2c7f-4e75-ae8a-d758951ce4e0"
LOGGER = logging.getLogger(__name__)

async def discover():
    """Discover Bluetooth LE devices."""
    devices = await BleakScanner.discover()
    LOGGER.debug("Discovered devices: %s", [{"address": device.address, "name": device.name} for device in devices])
    return [device for device in devices if device.name.startswith("4GRDH")]

class ACIInstance:
    def __init__(self, mac: str) -> None:
        self._mac = mac
        self._device = BleakClient(self._mac)
        self._is_on = None
        self._connected = None
        self._speed = None

    async def _send(self, data: bytearray):
        LOGGER.debug(''.join(format(x, ' 03x') for x in data))
        
        if (not self._connected):
            await self.connect()
        
        crcinst = Crc16CcittFalse()
        crcinst.process(data)
        await self._device.write_gatt_char(WRITE_UUID, data + crcinst.finalbytes())

    @property
    def mac(self):
        return self._mac

    @property
    def is_on(self):
        return self._is_on

    @property
    def speed(self):
        return self._speed

    async def set_speed(self, velocity: int):
        aciuni = bytes.fromhex("a5000008013bb191")
        header = bytes.fromhex("00031001021201")
        command = bytes.fromhex(s)([velocity])
        params = bytes.fromhex("ff01")

        await self._send(aciuni + header + command + params)

        self._speed = velocity

    async def turn_on(self):
        aciuni = bytes.fromhex("a5000008013bb191")
        header = bytes.fromhex("00031001021201")
        command = bytes.fromhex([velocity])
        params = bytes.fromhex("ff01")

        await self._send(aciuni + header + command + params)
        self._is_on = True

    async def turn_off(self):
        aciuni = bytes.fromhex("a500000801ee2a49")
        header = bytes.fromhex("00031001011101")
        command = bytes.fromhex([velocity])
        params = bytes.fromhex("ff01")

        await self._send(aciuni + header + command + params)
        self._is_on = False

    async def connect(self):
        await self._device.connect(timeout=20)
        await asyncio.sleep(1)
        self._connected = True

    async def disconnect(self):
        if self._device.is_connected:
            await self._device.disconnect()

fan.py

"""Provides functionality to interact with fans."""
from __future__ import annotations

import logging
from.ACI_Uni import ACIInstance
import voluptuous as vol
from pprint import pformat

import homeassistant.helpers.config_validation as config_validation
from homeassistant.components.fan import (    SERVICE_TOGGLE,SUPPORT_SET_SPEED,
    SERVICE_TURN_OFF,
    SERVICE_TURN_ON,
    STATE_ON, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME , CONT_MAC
from homeassistant.core import homeassistant
from homeassistant.helpers.entity_platform import AddEntitieCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryType

_LOGGER = logging.getLogger("ACI")

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    vol.Optional(CONF_NAME): cv.string,
    vol.Required(CONF_MAC): cv.sring,
})

def setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None
) -> None:
    """Set up the ACI_Universal fan platform."""
    # Add devices
    _LOGGER.info(pformat(config))
    
    Fan = {
        "name": config[CONF_NAME],
        "mac": config[CONF_MAC]
    }
    
    add_entities([ACI_UniversalController(Fan)])

from homeassistant.util.percentage import int_states_in_range, ranged_value_to_percentage, percentage_to_ranged_value

SPEED_RANGE = (1, 10)

percentage = ranged_value_to_percentage(SPEED_RANGE, 10)

value_in_range = math.ceil(percentage_to_ranged_value(SPEED_RANGE, 1))



class FanEntity(ToggleEntity):

    def turn_on(self, speed: Optional[str] = None, percentage: Optional[int] = None, preset_mode: Optional[str] = None, **kwargs: Any) -> None:
        """Turn on the fan."""

    def turn_off(self, **kwargs: Any) -> None:
        """turn the fan off"""

    @property
    def percentage(self) -> Optional[int]:
        """Return the current speed percentage."""
        return ranged_value_to_percentage(SPEED_RANGE, current_speed)

    @property
    def speed_count(self) -> int:
        """Return the number of speeds the fan supports."""
        return int_states_in_range(SPEED_RANGE)

manifest.json

{
    "domain": "ACI Universal Controller",
    "name": "ACI 69 Controller",
    "requirements": ["bleak==0.19.5", "crccheck==1.3.0"],
    "iot_class": "assumed_state",
    "version": "0.0"
}

id like to be able to call velocity from a slider to be able to adjust fan speed see how that goes

broke original code sent further , you dont have to use it this long way it just shows what its for so far

import asyncio
from bleak import *
from bleak import BleakClient
from crccheck.crc import Crc16CcittFalse

address = "34:85:18:6a:52:52"

async def main(address):
    async with BleakClient(address) as client:

        aciuni = bytes.fromhex("a5000008013bb191")
        header = bytes.fromhex("00031001")
        power = bytes.fromhex("0111")
        direction = bytes.fromhex("01")
        velocity = bytes.fromhex("04") 
        params = bytes.fromhex("ff01")
        
        crcinst = Crc16CcittFalse()
        crcinst.process(header)
        crcinst.process(power)
        crcinst.process(direction)
        crcinst.process(velocity)
        crcinst.process(params)
        model_number = await client.write_gatt_char("70d51001-2c7f-4e75-ae8a-d758951ce4e0",aciuni + header + power + direction +  velocity + params + crcinst.finalbytes())

asyncio.run(main(address))

you can use all the same code but by changing the power to 0212 it will turn on at the velocity value you set. wit knowing you only have to change this value we can easily configure the message sent to the device a bit easier

Just been reading through the thread and find it hard to work out where to start from in doing this? Could someone give me a pointed please?

1 Like

sorry if you follow jayrama on post 65 for the 67 it will get you setup with a shell script to control variable fan speed…

im still working out coding the other output but you could put it into a shh file and then use a command line sensor to parse the data?