@potterdai/homebridge-am43-blinds
Version:
A homebridge plugin to control AM43 based shade motors in HomeKit. These include the A OK and Zemismart bluetooth based motors. This plugin requires a Homebridge host that supports Bluetooth 4.0
397 lines (346 loc) • 12.1 kB
JavaScript
const EventEmitter = require("events").EventEmitter
const debug = require("debug")("AM43")
const StaticVariables = {
AM43_SERVICE_ID: "fe50",
AM43_CHARACTERISTIC_ID: "fe51",
AM43_COMMAND_PREFIX: Uint8Array.from([0x00, 0xff, 0x00, 0x00, 0x9a]),
AM43_COMMAND_ID_SET_MOVE: 0x0a,
AM43_MOVE_OPEN: 0xdd,
AM43_MOVE_CLOSE: 0xee,
AM43_MOVE_STOP: 0xcc,
AM43_COMMAND_ID_SET_POSITION: 0x0d,
AM43_COMMAND_ID_GET_POSITION: 0xa7,
AM43_COMMAND_ID_GET_LIGHTSENSOR: 0xaa,
AM43_COMMAND_ID_GET_BATTERYSTATUS: 0xa2,
AM43_RESPONSE_ACK: 0x5a,
AM43_RESPONSE_NACK: 0xa5,
AM43_NOTIFY_POSITION: 0xa1,
POSITION_HISTORY_LENGTH: 6,
}
class AM43Device extends EventEmitter {
constructor(peripheral) {
super()
this.peripheral = peripheral
if (peripheral.localName) {
this.name = peripheral.localName
} else if (peripheral.advertisement.localName) {
this.name = peripheral.advertisement.localName
} else if (peripheral.address) {
this.name = peripheral.address
} else {
let name = "AM43 Blind"
if (peripheral.id) {
name += " "
name += peripheral.id
}
this.name = name
}
if (peripheral.id) {
this.id = peripheral.id
} else if (peripheral.uuid) {
this.id = peripheral.uuid
} else if (peripheral.address) {
this.id = peripheral.address
} else {
this.id = this.name
}
this.address = peripheral.address
let addressDesc =
this.peripheral.address != null
? this.peripheral.address
: this.peripheral.id
this.description = this.name + " (" + addressDesc + ")"
this.isConnected = false
this.peripheral.on("connect", () => {
this.logDebug(`Device connected: ${this.id}`)
this.isConnected = true
})
this.peripheral.on("disconnect", () => {
this.logDebug(`Device disconnected: ${this.id}`)
this.isConnected = false
})
this.blindsControlCharacteristic = null
this.position = 0
this.targetPosition = null
this.direction = 2 // 0: Down/Decreating, 1: Up/Increasing, 2: Stopped
this.batteryPercentage = 50
this.positionHistory = []
}
logDebug(info) {
if (this.log) {
this.log.debug(`${this.description}: ${info}`)
} else {
debug(`${this.description}: ${info}`)
}
}
logInfo(info) {
if (this.log) {
this.log.info(`${this.description}: ${info}`)
} else {
debug(`${this.description}: ${info}`)
}
}
setBlindsControlCharacteristic(characteristic) {
this.blindsControlCharacteristic = characteristic
this.blindsControlCharacteristic.on("data", (data) => {
this.logDebug("--------Notification--------")
const dataArray = new Uint8Array(data)
this.logDebug(`Data received:` + dataArray)
let percentage = null
switch (data[1]) {
case StaticVariables.AM43_COMMAND_ID_GET_POSITION:
this.logDebug("Position update received")
percentage = parseInt(dataArray[5])
if (percentage >= 99) percentage = 100
if (percentage <= 1) percentage = 0
this.logDebug(`Closed Percentage ${percentage}`)
this.position = percentage
this.positionHistory.unshift(percentage)
this.positionHistory.length = Math.min(
StaticVariables.POSITION_HISTORY_LENGTH,
this.positionHistory.length
)
this.emit("position", this.position)
break
case StaticVariables.AM43_COMMAND_ID_GET_LIGHTSENSOR:
this.logDebug("light sensor update received")
percentage = parseInt(dataArray[4])
this.logDebug(`Light level ${percentage}`)
this.emit("lightLevel", percentage)
break
case StaticVariables.AM43_COMMAND_ID_GET_BATTERYSTATUS:
this.logDebug("Battery Status update received")
percentage = parseInt(dataArray[7])
this.logDebug(`Battery Percentage ${percentage}`)
this.batteryPercentage = percentage
this.emit("batteryPercentage", this.batteryPercentage)
break
case StaticVariables.AM43_NOTIFY_POSITION:
this.logDebug("Position notify received")
percentage = parseInt(dataArray[4])
this.logDebug(`Closed Percentage ${percentage}`)
this.position = percentage
this.positionHistory.unshift(percentage)
this.positionHistory.length = Math.min(
StaticVariables.POSITION_HISTORY_LENGTH,
this.positionHistory.length
)
this.emit("position", this.position)
break
case StaticVariables.AM43_COMMAND_ID_SET_MOVE:
this.logDebug("Set move notify received")
if (dataArray[3] == StaticVariables.AM43_RESPONSE_ACK) {
this.logDebug("Set move acknowledged")
} else if (dataArray[3] == AM43_RESPONSE_NACK) {
this.logDebug("Set move denied")
}
break
case StaticVariables.AM43_COMMAND_ID_SET_POSITION:
this.logDebug("Set position notify received")
if (dataArray[3] == StaticVariables.AM43_RESPONSE_ACK) {
this.logDebug("Set position acknowledged")
} else if (dataArray[3] == StaticVariables.AM43_RESPONSE_NACK) {
this.logDebug("Set position denied")
}
break
default:
break
}
if (this.targetPosition != null && this.position != null) {
let direction = this.targetPosition < this.position ? 1 : 0
let targetPosition = this.targetPosition
if (this.position == this.targetPosition || this.checkIfStopped()) {
this.logDebug(
`Target position ${this.targetPosition} reached @ ${this.position}`
)
targetPosition = null
}
if (targetPosition == null) {
direction = 2
}
if (direction != this.direction) {
this.direction = direction
this.emit("direction", this.direction)
}
if (targetPosition != this.targetPosition) {
this.targetPosition = targetPosition
this.emit("targetPosition", this.targetPosition)
}
}
})
this.blindsControlCharacteristic.subscribe((error) => {
if (error) {
this.logDebug("Failed to subsribe to notifications")
} else {
this.logDebug("Subscribed to notifications")
}
})
this.connectingPromise = null
this.discoveringPromise = null
}
async prepareAsync() {
if (!this.isConnected) {
await this.connectAsync()
}
await this.updatePositionAsync()
}
async connectAsync() {
this.logDebug("connectAsync")
if (!this.isConnected) {
if (this.connectingPromise == null) {
this.connectingPromise = this.peripheral.connectAsync()
}
await this.connectingPromise
this.connectingPromise = null
await new Promise((r) => setTimeout(r, 200))
if (!this.isConnected) {
return
}
}
if (this.blindsControlCharacteristic == null) {
if (this.discoveringPromise == null) {
this.logDebug("waitingForDiscoveringPromise")
this.discoveringPromise = this.peripheral.discoverSomeServicesAndCharacteristicsAsync(
[StaticVariables.AM43_SERVICE_ID],
[StaticVariables.AM43_CHARACTERISTIC_ID]
)
const { characteristics } = await this.discoveringPromise
this.discoveringPromise = null
this.logDebug("setBlindsControlCharacteristic")
this.setBlindsControlCharacteristic(characteristics[0])
} else {
await this.discoveringPromise
}
}
}
async disconnectAsync() {
this.logDebug("disconnectAsync")
this.isConnected = false
this.connectingPromise = null
this.discoveringPromise = null
this.blindsControlCharacteristic = null
await this.peripheral.disconnectAsync()
}
async enableNotificationsAsync() {
try {
await this.blindsControlCharacteristic.subscribeAsync()
this.logDebug("Subscribed to notifications")
} catch (e) {
this.logDebug("Failed to subsribe to notifications")
}
}
async setPositionAsync(position, trackPosition) {
this.targetPosition = position
await this.sendCommandAsync(StaticVariables.AM43_COMMAND_ID_SET_POSITION, [
position,
])
if (trackPosition == true) {
this.positionHistory = []
this.trackCurrentPosition()
}
}
trackCurrentPosition() {
setTimeout(async () => {
await this.updatePositionAsync()
if (this.targetPosition != null) {
this.trackCurrentPosition()
}
}, 1000)
}
checkIfStopped() {
this.logDebug("[checkIfStopped] " + this.positionHistory)
if (this.positionHistory.length < StaticVariables.POSITION_HISTORY_LENGTH)
return false
return this.positionHistory.every((v) => v === this.positionHistory[0])
}
async openAsync() {
this.targetPosition = 0
this.direction = 1
await this.sendCommandAsync(StaticVariables.AM43_COMMAND_ID_SET_MOVE, [
AM43_MOVE_OPEN,
])
this.emit("direction", this.direction)
this.emit("targetPosition", this.targetPosition)
}
async closeAsync() {
this.targetPosition = 100
this.direction = 0
await this.sendCommandAsync(StaticVariables.AM43_COMMAND_ID_SET_MOVE, [
AM43_MOVE_CLOSE,
])
this.emit("direction", this.direction)
this.emit("targetPosition", this.targetPosition)
}
async stopAsync() {
this.targetPosition = null
this.direction = 2
await this.sendCommandAsync(StaticVariables.AM43_COMMAND_ID_SET_MOVE, [
AM43_MOVE_STOP,
])
this.emit("direction", this.direction)
this.emit("targetPosition", this.targetPosition)
}
async updatePositionAsync() {
await this.sendCommandAsync(StaticVariables.AM43_COMMAND_ID_GET_POSITION, [
0x1,
])
}
async updateBatteryStatusAsync() {
await this.sendCommandAsync(
StaticVariables.AM43_COMMAND_ID_GET_BATTERYSTATUS,
[0x1]
)
}
async updateLightSensorAsync() {
await this.sendCommandAsync(
StaticVariables.AM43_COMMAND_ID_GET_LIGHTSENSOR,
[0x1]
)
}
async sendCommandAsync(commandID, data) {
if (!this.isConnected || this.blindsControlCharacteristic == null || this.blindsControlCharacteristic == undefined) {
await this.connectAsync()
if (!this.isConnected || this.blindsControlCharacteristic == null || this.blindsControlCharacteristic == undefined) {
return
}
}
this.logDebug("--------Command--------")
this.logDebug(`Sending command to device: ${this.id}`)
const bufferArray = new Uint8Array(data.length + 8)
const startPackage = StaticVariables.AM43_COMMAND_PREFIX
for (let index = 0; index < startPackage.length; index++) {
bufferArray[index] = startPackage[index]
}
bufferArray[5] = commandID
const uIntData = Uint8Array.from(data)
bufferArray[6] = uIntData.length
let bufferIndex = 7
for (let index = 0; index < uIntData.length; index++) {
bufferArray[bufferIndex] = uIntData[index]
bufferIndex++
}
bufferArray[bufferIndex] = this.calculateCommandChecksum(bufferArray)
bufferIndex++
const buffer = Buffer.from(bufferArray.buffer)
let hexString = buffer.toString("hex")
this.logDebug(`Sending command: ${hexString}`)
this.logDebug(`Blinds characteristic available: ${this.blindsControlCharacteristic !== null && this.blindsControlCharacteristic !== undefined}`)
try {
await this.blindsControlCharacteristic.writeAsync(buffer, true)
} catch (error) {
this.logDebug(`Failed write command with error: ${error}`)
}
}
calculateCommandChecksum(bufferArray) {
let checksum = 0
for (let i = 0; i < bufferArray.length - 1; i++) {
checksum = checksum ^ bufferArray[i]
}
checksum = checksum ^ 0xff
return checksum
}
}
module.exports = {
AM43Device: AM43Device,
StaticVariables: StaticVariables,
}