xiaomi2mqtt
Version:
A bridge between the Xiaomi home-automation gateway and a MQTT server.
286 lines (256 loc) • 8.06 kB
JavaScript
const log = require('yalm')
const config = require('./config.js')
const mqtt = require('mqtt')
const Aqara = require('lumi-aqara')
const pkg = require('../package.json')
const fs = require('fs')
let mqttClient
let aqara
let devices
let gateways = []
function start () {
log.setLevel(config.logging)
log._info(pkg.name + ' ' + pkg.version + ' starting')
if (config.devices) {
fs.access(config.devices, fs.constants.R_OK, (err) => {
if (!err) {
log.info('Loading devices from: ' + config.devices)
try {
devices = require(config.devices)
} catch (e) {
log.error('Error loading devices: ', e)
}
}
})
}
const mqttOptions = {
will: {
topic: config.name + '/connected',
message: 0,
qos: 0,
retain: true
},
rejectUnauthorized: !config.insecure
}
mqttClient = mqtt.connect(config.mqtt, mqttOptions)
mqttClient.on('connect', () => {
log.info('Connected to MQTT: %s', config.mqtt)
publishConnectionStatus()
mqttClient.subscribe(config.name + '/set/+/light')
})
mqttClient.on('message', (topic, message) => {
const parts = topic.split('/')
if (parts[1] === 'set' && parts[3] === 'light') {
const gateway = gateways[parts[2]]
if (gateway) {
handleGatewayLightUpdate(gateway, message)
}
}
})
mqttClient.on('close', () => {
log.info('mqtt closed ' + config.mqtt)
})
mqttClient.on('error', err => {
log.error('mqtt', err.toString())
})
mqttClient.on('offline', () => {
log.error('mqtt offline')
})
mqttClient.on('reconnect', () => {
log.info('mqtt reconnect')
})
aqara = new Aqara()
aqara.on('gateway', (gateway) => {
log.info('Gateway discovered')
gateway.on('ready', () => {
log.info('Gateway %s ready', gateway._sid)
gateways[gateway._sid] = gateway
publishConnectionStatus()
if (devices && devices.gateways && devices.gateways[gateway._sid]) {
gateway.setPassword(devices.gateways[gateway._sid])
} else if (config.password) {
gateway.setPassword(config.password)
}
})
gateway.on('offline', () => {
delete gateways[gateway._sid]
log.warn('Gateway is offline')
publishConnectionStatus()
})
gateway.on('subdevice', (device) => {
log.debug(`Device found: ${device.getSid()} (${device.getType()}) name: ${getFriendlyName(device.getSid())}`)
switch (device.getType()) {
case 'magnet':
publishMagnetState(device, `${device.isOpen() ? 'open' : 'closed'}`)
device.on('open', () => {
publishMagnetState(device, 'open')
})
device.on('close', () => {
publishMagnetState(device, 'closed')
})
break
case 'switch':
publishDeviceData(device, 'unknown')
device.on('click', () => {
publishDeviceData(device, 'clicked')
})
device.on('doubleClick', () => {
publishDeviceData(device, 'double_clicked')
})
device.on('longClickPress', () => {
publishDeviceData(device, 'pressed')
})
device.on('longClickRelease', () => {
publishDeviceData(device, 'released')
})
break
case 'motion':
publishDeviceData(device, `${device.hasMotion() ? 'motion' : 'no_motion'}`, { lux: device.getLux() })
device.on('motion', () => {
publishDeviceData(device, 'motion', { lux: device.getLux() })
})
device.on('noMotion', () => {
publishDeviceData(device, 'no_motion', { secondsSinceMotion: device.getSecondsSinceMotion(), lux: device.getLux() })
})
break
case 'sensor':
publishHTSensor(device)
device.on('update', () => {
publishHTSensor(device)
})
break
case 'leak':
publishDeviceData(device, `(${device.isLeaking() ? 'leaking' : 'not_leaking'})`)
device.on('update', () => {
publishDeviceData(device, `(${device.isLeaking() ? 'leaking' : 'not_leaking'})`)
})
break
case 'cube':
publishDeviceData(device, 'unknown')
device.on('update', () => {
publishDeviceData(device, device.getStatus(), { rotation: device.getRotateDegrees() })
})
break
}
})
gateway.on('lightState', (state) => {
log.info(`Light updated: ${JSON.stringify(state)}`)
const data = {
state: state,
ts: Date.now()
}
const sid = gateway._sid
mqttClient.publish(`${config.name}/status/light/${sid}`,
JSON.stringify(data),
{ qos: 0, retain: true })
})
})
}
function handleGatewayLightUpdate (gateway, message) {
if (!gateway) return
// TODO send message to gateway.
log.info('Updating gateway light')
if (IsNumeric(message)) {
var value = parseInt(message)
if (value >= 0 && value <= 100) {
gateway.setIntensity(value)
} else {
log.warn(`Value: ${value} not valid intensity!`)
}
} else { // Not numeric
const data = JSON.parse(message)
// TODO do something with the data.
gateway.setIntensity(data.intensity)
gateway.setColor(data.color)
}
}
function publishConnectionStatus () {
var status = '1'
if (gateways.length > 0) { status = '2' }
mqttClient.publish(config.name + '/connected', status, {
qos: 0,
retain: true
})
}
function publishDeviceData (device, newState, extraData = {}) {
let data = {
val: newState, // Using val according to the MQTT Smarthome specs.
battery: device.getBatteryPercentage(),
name: getFriendlyName(device.getSid()),
ts: Date.now()
}
Object.assign(data, extraData)
var topic = `${config.name}/status/${device.getType()}/${device.getSid()}`
log.info(`Publishing ${newState} to ${topic}`)
mqttClient.publish(topic,
JSON.stringify(data),
{ qos: 0, retain: true }
)
}
let magnets = []
function publishMagnetState (device, newState) {
const magnetIndex = magnets.findIndex(function (m) { return m.id === device.getSid() })
if (magnetIndex > -1) {
if (magnets[magnetIndex].state === newState) {
return
} else {
magnets[magnetIndex].state = newState
}
} else {
magnets.push({ id: device.getSid(), state: newState })
}
publishDeviceData(device, newState)
publishOpenMagnetCount()
}
function publishOpenMagnetCount () {
const openMagnets = magnets.filter(m => m.state === 'open')
let data = {
val: openMagnets.length,
name: 'All closed',
ts: Date.now()
}
if (openMagnets.length > 0) {
data.ids = openMagnets.map(m => m.id)
data.name = openMagnets.map(m => getFriendlyName(m.id)).sort().join(', ')
}
mqttClient.publish(`${config.name}/status/magnets`, JSON.stringify(data), { qos: 0, retain: true })
}
function publishHTSensor (sensorDevice) {
const tempTopic = `${config.name}/status/temperature/${sensorDevice.getSid()}`
const humTopic = `${config.name}/status/humidity/${sensorDevice.getSid()}`
const presTopic = `${config.name}/status/pressure/${sensorDevice.getSid()}`
let data = {
val: sensorDevice.getTemperature(),
battery: sensorDevice.getBatteryPercentage(),
name: getFriendlyName(sensorDevice.getSid()),
ts: Date.now()
}
mqttClient.publish(tempTopic,
JSON.stringify(data),
{ qos: 0, retain: true }
)
data.val = sensorDevice.getHumidity()
mqttClient.publish(humTopic,
JSON.stringify(data),
{ qos: 0, retain: true }
)
let pressure = sensorDevice.getPressure()
if (pressure !== null) {
data.val = pressure
mqttClient.publish(presTopic,
JSON.stringify(data),
{ qos: 0, retain: true }
)
}
}
// Usefull function
function IsNumeric (val) {
return Number(parseFloat(val)) === val
}
function getFriendlyName (deviceId) {
if (devices && devices[deviceId]) {
return devices[deviceId]
}
return null
}
start()