I have started putting up some notes on the API here. There is a lot more that’s simple to figure out using the LinQ tool, but this is what I have so far. It’s a wiki page to make it easy to edit: https://github.com/sillyfrog/aiopulse2/wiki/API
I have also contacted Automate to see if we can get some more info on the v2 API.
I can’t get UDP broadcast working at all. This would suggest that a substantial rewrite of aiopulse
would be required, as you suggest. @atmurray hasn’t been active for several months, so not sure what the next step would be.
sillyfrog, did you see JM123’s post from March 21? They received some documentation from the manufacturer which may be useful.
Here is some groovy code that enables some basic functionality on Hubitat.
JM123’s post lists the communication commands available in the Pulse Linq software, and also seems to link to an early version of Pulse Linq.
Thanks for that Hubitat implementation. It is clear that the Hub v2 can be easily discovered and controlled by sending commands over TCP on the local network. Aside from working out how to calculate battery percentage, JM123’s post and Pulse Linq document all the commands required for a useful integration I think.
The task now seems to be to rebuild the aiopulse
API (which otherwise is a nice implementation using asyncio
) and then update the HA intergration. Another option would be a more direct port of Pulse Linq’s node
js based control scheme, since the commands and logic are all there. Not sure if any HA integrations use node
based backends.
I may have spent a bit long on the battery - it’s what I guessed in that it shows the battery voltage (not sure of their formula to convert that to a percentage however, but I plan to ask). They came back with the docs that outline the protocol which is great, download from here: https://github.com/sillyfrog/aiopulse2/issues/1
As per @pss I think that means building an aiopulse2
API will be the way to go.
Thanks for this.
Any idea what the codes after the comma in the voltage report mean? Eg pVc01162,R7C.
The snippet below successfully sends messages to my v2 hub over TCP using asyncio
. I am a python noob so exclude glaring mistakes.
import asyncio
async def hub_send(message):
reader, writer = await asyncio.open_connection('XX.XX.XX.XX', 1487)
print(f'Send: {message!r}')
writer.write(message.encode())
data = await reader.read(100)
print(f'Received: {data.decode()!r}')
print('Close the connection')
writer.close()
asyncio.run(hub_send("!000NAME?;"))
No, unsure what the value after the R
is, it’s very weird as it does not appear to have much of a pattern. At first it was going up (in hex), but then started going back down. I have asked the question so will see if I get a reply.
For testing I have just been using telnet
/nc
/netcat
to see replies (rather than code).
Looking forward, given how the protocol works (ie: it pushes events as well as replies to queries), I think the implementation will need to have an event receiver that handles all (relevant) incoming messages, without awareness of what was sent (ie: stateless). So if we need to do something such as update position, it will send out the request, and not wait for the reply, rather when a reply comes in to the event handler, it will update HA as appropriate (this is more of a brain dump while I think of it). When/if I get a chance to start on some code, I’ll push to that repo regularly.
Cheers.
I had been playing around a little with it myself, just for fun. I hadn’t got real far, just working on pulling the hub details, different motors, and listening for position changes. It wasn’t built in a manner that would be super robust (or even complete, like the parse function etc), but was just exploring the different options and responses.
import os, sys, string, telnetlib, time
import asyncio
# Representation of an acmeda motor. Current has ID, Name, Position and Voltage
class AcmedaMotor:
def __init(self):
self.id = ""
self.name = ""
self.position = 0
self.voltage = 0
def get_id(self):
return self.id
def set_id(self, id):
self.id = id
def get_name(self):
return name.id
def set_name(self, name):
self.name = name
def get_position(self):
return self.position
def set_id(self, position):
self.position = position
def get_voltage(self):
return self.voltage
def set_voltage(self, position):
self.voltage = voltage
# Hub Communications
class AutomatePulse2:
HUB = ""
IP = ""
MOTORS_DICT = {}
def __init__(self, ip):
self.tn = telnetlib.Telnet()
self.IP = ip
self.openconnection()
def openconnection(self):
print("Connection Open")
self.tn.open(self.IP, "1487", 5)
def closeconnection(self):
print("Connection Closed")
self.tn.close()
# Once connection is open, get the nname of the Hub
def gethubname(self):
command = "!000NAME?;"
self.tn.write(command.encode("ascii"))
status = self.tn.read_until(b";",5)
self.HUB = status.decode("ascii").strip().replace(command[0:8], "").replace(";","")
print("Hub Name: " + self.HUB)
# Find all motors on the hub. Only include motors that report AC or DC style at the moment
def findmotors(self):
findblinds = "!000v?;"
self.tn.write(findblinds.encode('ascii') + b"\r")
n = self.tn.read_until(b"\r", 2)
blinds = n.decode("ascii").strip()
allitems= blinds.split(';')
for itm in allitems:
if "vD" in itm or "vA" in itm:
blind = itm[1:4]
if self.MOTORS_DICT.get(blind) == None:
self.MOTORS_DICT[blind] = ""
# Once we've found motors, we need to find the friendly name. While we're here, we can also check the voltage if it's a DC motor.
# Can probably work out the battery level from the reported voltage.
def getblindnames(self):
for motor in self.MOTORS_DICT:
command = "!" + motor + "NAME?;"
self.tn.write(command.encode("ascii"))
status = self.tn.read_until(b";",2)
str_status = status.decode("ascii").strip().replace("!" + motor + "NAME", "").replace(";","")
voltage = self.getblindvoltage(motor)
NewMotor = AcmedaMotor()
NewMotor.id = motor
NewMotor.name = str_status
NewMotor.voltage = voltage
self.MOTORS_DICT[motor] = NewMotor
self.listen()
def getblindvoltage(self, motor):
command = "!" + motor + "pVc?;"
self.tn.write(command.encode("ascii"))
status = self.tn.read_until(b";",2)
str_status = status.decode("ascii").strip().replace("!" + motor + "pVc", "").replace(";","")
return str_status
# Once we've got all the details, listen for an update. Once an update is received, parse the response, do whatever we need to do, and then
# listen for the next update.
def listen(self):
status = self.tn.read_until(b";")
str_status = status.decode("ascii").strip()
print("Status Update: " + str_status)
self.parse(str_status)
self.listen()
def parse(self, msg):
print("Event Received: " + msg)
motorAddress = msg[1:4]
lastThree = msg[-4:-1]
print(motorAddress)
print(lastThree)
#Ignore Telnet Errors and Hub Events
if motorAddress == "EUC" or motorAddress == "BR1" or lastThree == "Enp":
return
x = AutomatePulse2("10.0.0.188")
x.gethubname()
x.findmotors()
x.getblindnames()
I thought the RXX might be signal strength somehow, as this is reported in the app but I haven’t seen the read out anywhere else. My most distant blind reads R4E, R4F, R50, R51, and the closest reads R6D, R6A, R6C, R6D. I suppose the signal strength may increase as the battery becomes more charged, so there could be some correlation.
Regarding implementation, I have noted with Pulse Linq that some of the replies arrive very late, >5 minutes after the request in some cases. Admittedly, this is usually when all device info is being requested. Individual device commands seem to have less/little response latency.
@sillyfrog, nice documentation find!
I agree with @pss that the ,RXX
response suffix looks like a signal strength indicator.
-
,R5C
shows up with a green signal strength indicator in their app- (looks like
(((.)))
in the top right of the roller detail view
- (looks like
-
,R58
shows up as yellow -((.))
-
0x58 == 88
and0x5C == 92
. Perhaps0x5A (90)
separates green and yellow?
-
- Not sure when it goes below yellow.
Searching for rollease dc motor voltage
shows that these blinds use 12V. Thus I believe the pVc
message data indicates volts * 100
:
-
!XYZpVc01234,R88
indicates a battery at12.34V
-
!XYZpVc01122,R88
indicates a battery at11.22V
.
I do not have good precision on the separation between green and yellow yet:
-
1188
and above indicates green/full on their app -
1114
indicates yellow on their app.- Not sure where between 1188 and 1114 it changes
- No idea what voltage indicates red yet.
- Nor the voltage when it starts beeping as it rolls up (critically low)
Just need to be aware that if you have an open telnet connection to the hub, then any other connection will fail. So if any component was based off the telnet connection, then it will need to close the connection after every call, or you just won’t be able to use any other app.
In reality, I was having a bit of fun looking at it, but I think I’ll stick to using the HomeKit Controller integration in HA. The only thing it’s missing is battery level.
Does the HomeKit integration work well? Out of interest, what do you use it for that can’t be done in the Pulse App?
Personally, my main two use-cases for using the Integration over the Pulse App:
- Close blinds at sunset, rather than a static set time.
- Using my window sensors: if window open, only close the blind midway, so that the wind doesn’t constantly bang my blinds against the trim.
I’ve been using Homekit with v2 for a while, but I’m watching the discussions here closely because the HK is missing battery, and it doesn’t always report the state of the blinds properly (opening vs opened, closing vs closed, etc).
Does the HomeKit automation allow pairing/unpairing motors? Or does the HomeKit path require having an iOS device?
I have not attempted to use the Homekit Automation to pair, name, or organize my motors into “rooms”. All that work was done via the Pulse Automate app. I only use the Homekit Integration to raise, lower and query current status within HA.
I do not own anything iOS, so the Homekit Integration works without iOS.
The Pulse iOS App on the other hand appears to use Homekit for Hub Discovery.
So although I was able to configure the hub, unpair from Homekit, Pair to hass and use open my blinds; I’m then no-longer able to see or configure the hub within the iOS app.
I wrote a quick appdaemon app to fix the issue with state not reporting correctly. It just listens for the position change, when it changes to 0 or 100 it calls the cover.stop service. HA will then correctly report the state as open or closed.
It’s based off the appdaemon app I wrote so I could control my blinds with the light switches and Fibaro scene settings.
Now it’s only the lack of battery state that’s a problem for me.
Here is some code which demonstrates:
- Discovering hubs (by scanning nearby ip addresses listening on port 1487)
- Gathering the list of motors paired to each hub
- Following any updates to the motors
- Reports signal strength, battery level and when it starts/stops moving
- Picking a random motor and moving it to a random position after 10s, and then a second random position after another 10s.
- Exiting after 60s.
(fun aside: by putting a big piece of aluminum foil mostly over my hub I was able to reduce a signal strength reading to ,R4A
which registers as red (.)
in their app. Not sure the cutoff between red and yellow though)
For the battery voltage, I’ve started notating down voltages reported and the percentage in the app.
12.47 = 86%
12.25 = 81%
12.00 = 73%
10.99 = 46%
11.09 = 49%
11.54 = 59%
Thanks! Doing a linear regression on your data suggests the following equation:
battery_percent = 27.4*volts - 255