I’ve never heard about this brand, but looking at the product description, looks like it’s easier to retrieve information from the sensor.
Looks like Ecowitt have an integration in Home Assistant. If that works with their pool device it would be great.
Also found this as well for local.
No I dont have any other sensors you mention defined.
I have submitted a pull request for IBS-M1 support to Tuya Local (GitHub - make-all/tuya-local: Local support for Tuya devices in Home Assistant), it should support it since version 2023.7.0.
Hey All,
Just worked on this all weekend with the following:
IBS-M2S Wifi Base Station
ITH-20R-O Temp Sensors
All the previous people helped build the final solution and thankyou very much for that. Link the hub to your Tuya App on your phone. You’ll see in the first option a smiley face and some chinese writing - it loosely translates to add device. Slide this to 1,2,3,4 and each time power on and off your sensor by removing the battery. Eventually youll notice that the app will prompt you advising the snesor ch_0 has low battery, slide to the next 2,3,4 and repeat the process.
Do everything suggested above and create a tuya account
Link your account
You can either view the properties in the report or use the api explorer and query the properties.
The juicy stuff - it always defaults the value to F (just so you know)
This is the python code you can use to decode the values (so far I only have temp but i’m working on humidity).
import base64
#Decoding Why did i buy these Sensors InkBird Script
# Character conversion dictionary based on the provided method
character_conversion_new = {
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19,
'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29,
'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, 'Z': 35, 'a': 36, 'b': 37, 'c': 38, 'd': 39,
'e': 40, 'f': 41, 'g': 42, 'h': 43, 'i': 44, 'j': 45, 'k': 46, 'l': 47, 'm': 48, 'n': 49,
'o': 50, 'p': 51, 'q': 52, 'r': 53, 's': 54, 't': 55, 'u': 56, 'v': 57, 'w': 58, 'x': 59,
'y': 60, 'z': 61, '+': 62, '/': 63
}
def fahrenheit_to_celsius(f):
"""Convert Fahrenheit to Celsius."""
return (f - 32) * 5.0/9.0
# Applying the new decoding method on the initial encoded value
decoded_bytes = base64.b64decode("AXoAHwIUBf//YQ==")
raw_temp = decoded_bytes[8:11].decode(errors='ignore')
raw_temp_first_character = raw_temp[0:1]
raw_temp_second_character = raw_temp[1:2]
raw_temp_third_character = raw_temp[2:3]
temp_by_ten_new = (character_conversion_new[raw_temp_first_character] - 10) * 16 + character_conversion_new.get(raw_temp_second_character, 0)
if raw_temp_third_character == "B":
temp_by_ten_new += 256
temperature_f = temp_by_ten_new / 10
temperature_c= fahrenheit_to_celsius(temperature_f)
print(temperature_c)
Thanks to @markpurcell and everyone else, I, too have just gotten an IBS-M2 WiFi and IBS-P02R to work nicely. I could not figure out how to, so I never enabled “DP Mode” as some have said. I did set up the WiFi controller and pool thermometer with the Inkbird app, and then I reset the network settings and paired with Tuya app.
Mark’s script seemed to use a different version and method of the Tuya API, so I’ve updated to v2 and called a different method (properties). This gave me everything I needed.
#!/usr/bin/env python3
import json
import requests
import hmac
import hashlib
import time
import sys
# Declare constants
ClientID="<< Client ID >>"
ClientSecret="<< Client Secret >>"
DeviceID = "<< Device ID >>"
ChannelID = "<< Channel ID >>"
BaseUrl = "https://openapi.tuyaeu.com"
EmptyBodyEncoded = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# If there is an argument, override ChannelID with it
if len( sys.argv ) > 1:
ChannelID = sys.argv[1]
# Character conversion dictionary for decoding the temperature value
charcter_conversion = {
"A": 0, "E": 1, "I": 2, "M": 3, "Q": 4, "U": 5, "Y": 6, "c": 7,
"g": 8, "k": 9, "o": 10, "s": 11, "w": 12, "0": 13, "4": 14, "8": 15
}
# Get current time in milliseconds
tuyatime = int(time.time()) * 1000
# Prepare signing for Access Token
URL = "/v1.0/token?grant_type=1"
StringToSign = f"{ClientID}{tuyatime}GET\n{EmptyBodyEncoded}\n\n{URL}"
AccessTokenSign = hmac.new(ClientSecret.encode(), StringToSign.encode(), hashlib.sha256).hexdigest().upper()
# Get Access Token
headers = {
'client_id': ClientID,
'sign_method': 'HMAC-SHA256',
't': str(tuyatime),
'sign': AccessTokenSign
}
response = requests.get(f"{BaseUrl}{URL}", headers=headers)
AccessToken = response.json()['result']['access_token']
# Send Device status request
URL = f"/v2.0/cloud/thing/{DeviceID}/shadow/properties"
StringToSign = f"{ClientID}{AccessToken}{tuyatime}GET\n{EmptyBodyEncoded}\n\n{URL}"
RequestSign = hmac.new(ClientSecret.encode(), StringToSign.encode(), hashlib.sha256).hexdigest().upper()
headers.update({
'sign': RequestSign,
'access_token': AccessToken
})
response = requests.get(f"{BaseUrl}{URL}", headers=headers)
RequestResponse = response.text
# Parse and process the request response
device_param = json.loads(RequestResponse)
number_of_status = len(device_param["result"]["properties"])
for i in range(number_of_status):
# print(device_param["result"]["properties"][i]["code"])
if device_param["result"]["properties"][i]["code"] == ChannelID:
raw_temp = device_param["result"]["properties"][i]["value"]
# Decode the temperature value
raw_temp_first_charecter = raw_temp[1:2]
raw_temp_second_charecter = raw_temp[2:3]
raw_temp_third_charecter = raw_temp[3:4]
temp_by_ten = (ord(raw_temp_first_charecter) - 65)*16 + charcter_conversion[raw_temp_second_charecter]
if raw_temp_third_charecter == "B":
temp_by_ten = temp_by_ten + 256
# Calculate and print the pool temperature
Pool_temp = temp_by_ten / 10
print(Pool_temp)
And then, in configuration.yaml, I have:
command_line:
- sensor:
command: "python3 /config/scripts/get_pool_temp.py"
name: Pool Temperature
device_class: temperature
unit_of_measurement: "°C"
scan_interval: 60
unique_id: << use your own unique_id >>
I also added the ability to pass the ChannelID as an argument. There was some interesting value under “ch_0”. I THOUGHT it might be the battery sensor, so I just set up another command_line sensor as follows:
- sensor:
command: "python3 /config/scripts/get_pool_temp.py ch_0"
name: Pool Temperature Battery
device_class: battery
unit_of_measurement: "%"
scan_interval: 60
unique_id: << use your own unique_id >>
However, I don’t think it’s the battery, This is the past 24 hours:
Also, the pool thermometer fell offline from the WiFi base station until 8am, so it seems that is must be some data that must be coming directly from the WiFi vs the wireless pool thermometer.
@Nir @keeema Guys please tell me if I’m way off the mark here, but I found this thread because I’ve got an Inkbird hot tub/pool thermometer I’m wanting to integrate into HA, and seeing you guys doing some manual stab-in-the-dark character conversion thing for getting the temperature out of that string just seemed crazy!
Am I assuming right that the received data is a string like this? (from @keeema 's post above with a 19.8C reading) -
A8YAAAAAAAAARgD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AA==
If so, the “==” at the end stood out as it clearly being a b64 encoded string, with 0xff bytes causing all the “/” characters. I un-base64-ed some of your pasted data strings, converted to binary, and compared with the binary representation of the reported temperatures*10 (e.g. 261 instead of 26.1 degrees), and figured out that the second and third bytes extracted together are the temperature integer in little endian format.
So this code should work, for all cases except maybe negative values (since I didn’t have any negative examples to examine) -
>>> import base64
>>> code = "A8YAAAAAAAAARgD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AA=="
>>> int.from_bytes(base64.b64decode(code)[1:3], "little", signed=True) / 10
19.8
Again please let me know if I’m way off the mark here or have missed people figuring out a different/better way to get the data, but I’m also very curious to see what negative temperatures come out as!
Nice job! I’m sure that will help someone. Unfortunately, didn’t work for my string.
AP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wADYQEUBRQF//85AP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AA==
It returns -0.1.
Ah that’s a completely different format (edit - or temperature being reported from a different channel?) from the ones from Ni and keeema I was looking at above… I’ll give it a quick look though. Do you know what the actual temperature displayed at that point was?
So, I’m not home right now so I can’t tell you exactly. But using my code, supposed to be 35.2.
I might have it -
>>> import base64
>>> code = "AP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wADYQEUBRQF//85AP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AAD//////////wAA//////////8AAP//////////AA=="
>>> int.from_bytes(base64.b64decode(code)[51:53], "little", signed=True) / 10
35.3
If this happens to work for you, it might mean that your javascript thing needs this line (117):
let temperature = { sensor: (bytesSensor[51] + bytesSensor[15]) / 10};
Changing to say:
let temperature = { sensor: (bytesSensor[52] << 8 | bytesSensor[51]) / 10};
(Assuming that syntax for left-bitshifting and then bitwise ORing is correct, sorry I’m not familiar with js. Also I’m not sure why bytesSensor[15]
was being added on? It’s just one of the 0xff padding bytes, but I guess that may have had the side effect of giving the correct result (albeit 0.1 less) when the temp is above 25.5 i.e. a number of 256 or more and therefore requiring the extra low-end bit from the neighbouring byte.)
To be honest, I tried a several number of attempts until it match the temperature I was expecting. So far it works for values above 20 degrees. Anyway, I’ve changed the line like you suggested and it worked too.
@Colgate2in1 Thanks for that! Sorry for the delayed response, I only now found some time to look into what you suggested.
It is working for me. I was sure they use some encoding method and I tried to google it but didn’t really know where to begin as I have no background in this area. I eventually found a pattern that worked by looking at many examples and used that. No doubt that your solution is much more elegant and short!
No problem! To be honest it’s pretty impressive how far you got with effectively trying to reverse engineer base64 encoding without realising it. The data being sent little-endian just added on an extra layer of confusion that you can’t see without looking at the raw bytes.
I went down this path too, and still have some work on some of the fields, but foudn a clean way to parse the temps, battery and humidity (if applicable):
After I got this working I realize HA has migrated from the original API (openapi.tuyaus) via tuya-iot-python-sdk to the new sdk (apigw.tuyaus) via tuya-device-sharing-sdk
Unfortunately, even with the DP mode enabled the API doesn’t seem to send the additional fields. I’ve opened an issue: API Difference Causing issues w/ Home Assistant Integration · Issue #11 · tuya/tuya-device-sharing-sdk · GitHub I’d appreciate upvoting or commenting there – if they expand the signature of that API we’ll be able to merge my PR and get IBS-M2 data natively.
I forked the core Tuya integration, reverting back to the openapi sdk, bundled in my new sensor and added it to HACS as a custom integration – it’s installable from here:
For posterity’s sake, here’s the parsing code for the sensor data:
from typing import Tuple, Optional
import base64
import struct
def from_raw(data: str) -> Tuple[float, Optional[float], int]:
decoded_bytes = base64.b64decode(data)
# TODO: Identify what the skipped bytes are in the base station data
_temperature, _humidity, _, battery = struct.Struct("<hhIb").unpack(decoded_bytes[1:11])
(temperature, humidity) = _temperature / 10.0, _humidity / 10.0
if _humidity == -1: humidity = None
return temperature, humidity, battery
examples = [
"AXoAHwIUBf//YQ==",
'AdYAhwEAEA4AZA==',
'CKYA////////MQ==',
'CL8A////////ZA=='
]
for example in examples:
deg_c, humidity, battery = from_raw(example)
deg_f = (deg_c+32)*9/5
print(f"{example}: {deg_c:.1f}˚C / {deg_f:.1f}˚F @ {humidity}% Humidity & {battery}% Charge")
Amazing great !
Dont know why when configuring tuyaopenapi we dont know wich information we have to provide
As we have in original tuya intégration:
So after configuring corect information it work great
Many thanks for that Hacs intégration really cool
I have tried to configure the plugin and not sure what to use as an account or password.
If I use email and password, then it says Login error (2427): the user did not scan the code to bind the cloud project
If I use User Code and password, I get Login error (1106): permission deny
The original Tuya integration shown the QR code to scan but this one not so I am little bit confused.