UNPKG

homebridge-xiaomi-roborock-vacuum

Version:

Xiaomi Vacuum Cleaner - 1st (Mi Robot), 2nd (Roborock S50 + S55), 3rd Generation (Roborock S6) and S5 Max - plugin for Homebridge.

195 lines 6.94 kB
"use strict"; const EventEmitter = require("events"); const dgram = require("dgram"); const debug = require("debug"); const Packet = require("./packet"); const DeviceInfo = require("./device_info"); const PORT = 54321; /** * Class for keeping track of the current network of devices. This is used to * track a few things: * * 1) Mapping between adresses and device identifiers. Used when connecting to * a device directly via IP or hostname. * * 2) Mapping between id and detailed device info such as the model. * */ class Network extends EventEmitter { constructor() { super(); this.packet = new Packet(true); this.addresses = new Map(); this.devices = new Map(); this.references = 0; this.debug = debug("miio:network"); } search() { this.packet.handshake(); const data = Buffer.from(this.packet.raw); this.socket.send(data, 0, data.length, PORT, "255.255.255.255"); // Broadcast an extra time in 500 milliseconds in case the first brodcast misses a few devices setTimeout(() => { this.socket.send(data, 0, data.length, PORT, "255.255.255.255"); }, 500); } findDevice(id, rinfo) { // First step, check if we know about the device based on id let device = this.devices.get(id); if (!device && rinfo) { // If we have info about the address, try to resolve again device = this.addresses.get(rinfo.address); if (!device) { // No device found, keep track of this one device = new DeviceInfo(this, id, rinfo.address, rinfo.port); this.devices.set(id, device); this.addresses.set(rinfo.address, device); return device; } } return device; } async findDeviceViaAddress(options) { if (!this.socket) { throw new Error("Implementation issue: Using network without a reference"); } let device = this.addresses.get(options.address); if (!device) { // No device was found at the address, try to discover it device = new DeviceInfo(this, null, options.address, options.port || PORT); this.addresses.set(options.address, device); } // Update the token if we have one if (typeof options.token === "string") { device.token = Buffer.from(options.token, "hex"); } else if (options.token instanceof Buffer) { device.token = options.token; } // Set the model if provided if (!device.model && options.model) { device.model = options.model; } // Perform a handshake with the device to see if we can connect try { await device.handshake(); } catch (err) { // Supress missing tokens - enrich should take care of that if (err.code !== "missing-token") { throw err; } } const cachedDevice = this.cacheDevice(device); await cachedDevice.enrich(); return cachedDevice; } /** * Caches the device if not previously cached (could be the reason of failures when reconnecting?) * @private * @param device {@link DeviceInfo} * @returns {DeviceInfo} New device or the previously cached one */ cacheDevice(device) { if (!this.devices.has(device.id)) { // This is a new device, keep track of it this.devices.set(device.id, device); } // Sanity, make sure that the device in the map is returned return this.devices.get(device.id); } createSocket() { this._socket = dgram.createSocket("udp4"); // Bind the socket and when it is ready mark it for broadcasting this._socket.bind(); this._socket.on("listening", () => { this._socket.setBroadcast(true); const address = this._socket.address(); this.debug("Network bound to port", address.port); }); // On any incoming message, parse it, update the discovery this._socket.on("message", (msg, rinfo) => { const buf = Buffer.from(msg); try { this.packet.raw = buf; } catch (ex) { this.debug("Could not handle incoming message"); return; } if (!this.packet.deviceId) { this.debug("No device identifier in incoming packet"); return; } const device = this.findDevice(this.packet.deviceId, rinfo); device.onMessage(buf); if (!this.packet.data) { if (!device.enriched) { // This is the first time we see this device device .enrich() .then(() => { this.emit("device", device); }) .catch((err) => { this.emit("device", device); }); } else { this.emit("device", device); } } }); } list() { return this.devices.values(); } /** * Get a reference to the network. Helps with locking of a socket. */ ref() { this.debug("Grabbing reference to network"); this.references++; this.updateSocket(); let released = false; let self = this; return { release() { if (released) return; self.debug("Releasing reference to network"); released = true; self.references--; self.updateSocket(); }, }; } /** * Update wether the socket is available or not. Instead of always keeping * a socket we track if it is available to allow Node to exit if no * discovery or device is being used. */ updateSocket() { if (this.references === 0) { // No more references, kill the socket if (this._socket) { this.debug("Network no longer active, destroying socket"); this._socket.close(); this._socket = null; } } else if (this.references === 1 && !this._socket) { // This is the first reference, create the socket this.debug("Making network active, creating socket"); this.createSocket(); } } get socket() { if (!this._socket) { throw new Error("Network communication is unavailable, device might be destroyed"); } return this._socket; } } module.exports = new Network(); //# sourceMappingURL=network.js.map