-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4d04d7b
Showing
11 changed files
with
1,000 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
MQTT_URL=mqtt://keeper.local | ||
MESH_MQTT_PREFIX=mesh2mqtt | ||
HASS_MQTT_PREFIX=mesh2hass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/.vscode | ||
/node_modules | ||
keychain.json | ||
.env | ||
.seqs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"singleQuote": true, | ||
"trailingComma": "all" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
mesh2mqtt | ||
--------- | ||
|
||
A demo project for my smart house to make BLE Mesh and Home Assistant work together. | ||
|
||
System components: | ||
- ble mesh network (i'm using some yeelight bulbs and some custom nrf52 devices) | ||
- meshbridge - custom esp32 firmware to relay mesh messages to mqtt and vice versa | ||
- mesh2mqtt.js - handles all the mesh stack functions - crypto, segmentation, parsing, outputs nice jsons to mqtt topics | ||
- mesh2hass.js - listens those jsons and provides hass mqtt discovery and high-level control endpoints | ||
|
||
This does not currently support auto-discovery of mesh devices, so you should configure them manually in `devices.js` | ||
|
||
Supported device types: | ||
- `onoff-client-button` - mesh buttons (actually, anything using Generic OnOff Client can be configured as hass button) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = [ | ||
{ | ||
addr: '1006', | ||
type: 'onoff-client-button', | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"network": "1234567890abcdef1234567890abcdef", | ||
"devices": { | ||
"1": "1234567890abcdef1234567890abcdef", | ||
"2": "1234567890abcdef1234567890abcdef", | ||
"1234": "1234567890abcdef1234567890abcdef" | ||
}, | ||
"apps": { | ||
"blinker": "1234567890abcdef1234567890abcdef", | ||
"living-room": "1234567890abcdef1234567890abcdef" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
const fs = require('fs'); | ||
const debug = require('debug')('app'); | ||
const mqtt = require('./mqtt'); | ||
require('dotenv').config(); | ||
|
||
const devices = require('./devices'); | ||
|
||
const client = mqtt.connect(process.env.MQTT_URL || 'localhost'); | ||
const prefix = process.env.HASS_MQTT_PREFIX || 'mesh2hass'; | ||
const meshPrefix = process.env.MESH_MQTT_PREFIX || 'mesh2mqtt'; | ||
|
||
const transactionsSeen = new Set(); | ||
|
||
const deviceHandlers = { | ||
'onoff-client-button': async (config) => { | ||
const { addr } = config; | ||
|
||
await client.publish( | ||
`homeassistant/device_automation/${addr}/action_short_press/config`, | ||
JSON.stringify({ | ||
automation_type: 'trigger', | ||
device: { | ||
identifiers: [`blemesh_${addr}`], | ||
name: addr, | ||
}, | ||
payload: 'button_short_press', | ||
subtype: 'button_1', | ||
topic: `${prefix}/${addr}/action`, | ||
type: 'action', | ||
}), | ||
); | ||
|
||
await client.handle( | ||
`${meshPrefix}/${addr}/models/generic-onoff/set-unack`, | ||
(match, payload) => { | ||
const { status, transactionId } = JSON.parse(payload); | ||
const transactionKey = `${addr}-${transactionId}`; | ||
if (transactionsSeen.has(transactionKey)) { | ||
return; | ||
} | ||
|
||
transactionsSeen.add(transactionKey); | ||
client.publish(`${prefix}/${addr}/action`, 'button_short_press'); | ||
}, | ||
); | ||
}, | ||
}; | ||
|
||
client.on('connect', async () => { | ||
console.log('- setting up devices'); | ||
|
||
for (const device of devices) { | ||
const handler = deviceHandlers[device.type]; | ||
if (!handler) { | ||
console.error('Unsupported device type', device.type); | ||
continue; | ||
} | ||
|
||
console.log(`- [${device.addr}] ${device.type}`); | ||
handler(device); | ||
} | ||
}); | ||
|
||
// accessLayer.on('incoming', (msg) => { | ||
// debug('incoming message', msg); | ||
// const id = msg.meta.from.toString(16).padStart(4, '0'); | ||
|
||
// if (id === '1006') { | ||
// if (!devicesSeen.has(msg.meta.from)) { | ||
// devicesSeen.add(msg.meta.from); | ||
// client.publish( | ||
// `homeassistant/device_automation/${id}/action_short_press/config`, | ||
// JSON.stringify({ | ||
// automation_type: 'trigger', | ||
// device: { | ||
// identifiers: [`blemesh_${id}`], | ||
// name: id, | ||
// }, | ||
// payload: 'button_short_press', | ||
// subtype: 'button_1', | ||
// topic: `mesh2mqtt/devices/${id}/action`, | ||
// type: 'action', | ||
// }), | ||
// ); | ||
// } | ||
// if (msg.type === 'GenericOnOffSetUnacknowledged') { | ||
// const transactionKey = `${id}:${msg.payload.transactionId}`; | ||
// if (msg.payload.transactionId && transactionsSeen.has(transactionKey)) { | ||
// return; | ||
// } | ||
// client.publish(`${prefix}/devices/${id}/action`, 'button_short_press'); | ||
// transactionsSeen.add(transactionKey); | ||
// } | ||
// } | ||
|
||
// if (id === '0015') { | ||
// if (!devicesSeen.has(msg.meta.from)) { | ||
// devicesSeen.add(msg.meta.from); | ||
// client.publish( | ||
// `homeassistant/light/${id}/light/config`, | ||
// JSON.stringify({ | ||
// device: { | ||
// identifiers: [`blemesh_${id}`], | ||
// name: id, | ||
// }, | ||
// schema: 'json', | ||
// command_topic: `mesh2mqtt/devices/${id}/light/set`, | ||
// state_topic: `mesh2mqtt/devices/${id}/light/state`, | ||
// name: `${id}_light`, | ||
// unique_id: id, | ||
// }), | ||
// ); | ||
// } | ||
// if (msg.type === 'GenericOnOffStatus') { | ||
// client.publish( | ||
// `${prefix}/devices/${id}/light/status`, | ||
// JSON.stringify({ | ||
// state: `${msg.payload.currentStatus}`.toUpperCase(), | ||
// }), | ||
// ); | ||
// } | ||
// } | ||
// }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
const fs = require('fs'); | ||
const debug = require('debug')('app'); | ||
const { | ||
NetworkLayer, | ||
AccessLayer, | ||
UpperLayer, | ||
LowerLayer, | ||
Keychain, | ||
} = require('mesh-first-try'); | ||
const { kebabCase } = require('lodash'); | ||
const mqtt = require('./mqtt'); | ||
require('dotenv').config(); | ||
|
||
const client = mqtt.connect(process.env.MQTT_URL || 'localhost'); | ||
const prefix = process.env.MESH_MQTT_PREFIX || 'mesh2mqtt'; | ||
const ownMeshAddr = 0x7fc; | ||
let bridgeAddr; // FIXME | ||
|
||
const keychain = new Keychain(); | ||
keychain.load(require('./keychain.json')); | ||
|
||
// TODO: Make configurable | ||
const TypeToTopic = { | ||
GenericOnOffGet: ['generic-onoff', 'get'], | ||
GenericOnOffSet: ['generic-onoff', 'set'], | ||
GenericOnOffStatus: ['generic-onoff', 'status'], | ||
GenericOnOffSetUnacknowledged: ['generic-onoff', 'set-unack'], | ||
LightLightnessStatus: ['light-lightness', 'status'], | ||
LightLightnessSetUnacknowledged: ['light-lightness', 'set-unack'], | ||
LightCTLTemperatureStatus: ['light-ctl', 'temp-status'], | ||
LightCTLTemperatureSetUnacknowledged: ['light-ctl', 'temp-set-unack'], | ||
}; | ||
|
||
// Setup mesh stuff | ||
const networkLayer = new NetworkLayer(keychain); | ||
const lowerLayer = new LowerLayer(keychain); | ||
const upperLayer = new UpperLayer(); | ||
const accessLayer = new AccessLayer(); | ||
|
||
// Connect all the layers together | ||
networkLayer.on('incoming', (networkMessage) => { | ||
lowerLayer.handleIncoming(networkMessage); | ||
}); | ||
lowerLayer.on('incoming', (lowerTransportMessage) => { | ||
upperLayer.handleIncoming(lowerTransportMessage); | ||
}); | ||
upperLayer.on('incoming', (accessMessage) => { | ||
accessLayer.handleIncoming(accessMessage); | ||
}); | ||
accessLayer.on('outgoing', (accessMessage) => { | ||
upperLayer.handleOutgoing(accessMessage); | ||
}); | ||
upperLayer.on('outgoing', (lowerTransportMessage) => { | ||
lowerLayer.handleOutgoing(lowerTransportMessage); | ||
}); | ||
lowerLayer.on('outgoing', (networkMessage) => { | ||
networkLayer.handleOutgoing(networkMessage); | ||
}); | ||
|
||
// Handle all the mqtt topics | ||
client.on('connect', async () => { | ||
await client.publish(`${prefix}/online`, 'true'); | ||
|
||
// Route meshbridge messages from mqtt to mesh stack and back | ||
await client.handle('meshbridge/:addr/msg', (match, payload) => { | ||
bridgeAddr = match.params.addr; | ||
networkLayer.handleIncoming(payload); | ||
}); | ||
networkLayer.on('outgoing', async (payload) => { | ||
debug('sending', payload.toString('hex')); | ||
await client.publish(`meshbridge/${bridgeAddr}/msg/send`, payload); | ||
}); | ||
|
||
// Model behaviour, outgoing | ||
await client.handle( | ||
`${prefix}/:addr/models/:model/:op/send`, | ||
(match, payload) => { | ||
const { addr, model, op } = match.params; | ||
|
||
const type = Object.keys(TypeToTopic).find((key) => { | ||
const [modelName, opName] = TypeToTopic[key]; | ||
return modelName === model && opName === op; | ||
}); | ||
|
||
accessLayer.handleOutgoing({ | ||
type, | ||
appKey: 'mi', | ||
payload: JSON.parse(payload), | ||
meta: { | ||
to: Number.parseInt(addr, 16), | ||
from: ownMeshAddr, | ||
ttl: 5, | ||
}, | ||
}); | ||
}, | ||
); | ||
|
||
// Model behaviour, incoming | ||
accessLayer.on('incoming', async (msg) => { | ||
debug('incoming message', msg); | ||
const addr = msg.meta.from.toString(16).padStart(4, '0'); | ||
|
||
await client.publish(`${prefix}/rx`, JSON.stringify(msg)); | ||
|
||
const topic = TypeToTopic[msg.type]; | ||
if (topic) { | ||
await client.publish( | ||
`${prefix}/${addr}/models/${topic.join('/')}`, | ||
JSON.stringify(msg.payload), | ||
); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
const mqtt = require('async-mqtt'); | ||
const { parse, match } = require('path-to-regexp'); | ||
|
||
const patchClient = (client) => { | ||
client.handle = async (topicPattern, handler) => { | ||
const tokens = parse(topicPattern); | ||
const matcher = match(topicPattern); | ||
|
||
const wildcardTopic = tokens | ||
.map((v) => { | ||
if (typeof v === 'string') { | ||
return v; | ||
} | ||
return `${v.prefix}+`; | ||
}) | ||
.join(''); | ||
|
||
await client.subscribe(wildcardTopic); | ||
client.on('message', (topic, payload) => { | ||
const matched = matcher(topic); | ||
if (!matched) { | ||
return; | ||
} | ||
|
||
handler(matched, payload); | ||
}); | ||
}; | ||
}; | ||
|
||
module.exports = { | ||
connect: (...args) => { | ||
const client = mqtt.connect(...args); | ||
patchClient(client); | ||
return client; | ||
}, | ||
}; |
Oops, something went wrong.