import axios from "axios" import {EventEmitter} from "events" import * as camelCase from 'lodash/camelCase' import * as debounce from 'lodash/debounce' import * as find from 'lodash/find' import {connect, IClientPublishOptions, IPublishPacket, MqttClient} from 'mqtt' import {Alert} from "./Alert" import * as components from "./components" import {Component, ComponentConstructor} from './components' import {Device, IFirmwareRepositoryManifest, IHomieConfig} from "./Device" import * as locale from "./locale/pt" import {LogLine} from "./LogLine" import {RoomNotification} from "./Notification" import {Room} from "./Room" interface FirmwareUpdateSummary {updated: number; outdated: number; unknown: number;} export class FloraFi extends EventEmitter { private mqtt?: MqttClient = undefined id?: number name?: string legacyComponentSchema = false rooms: {[key: string]: Room} = {} alerts: Alert[] = [] devices: {[key: string]: Device} = {} logs: LogLine[] = [] MAX_LOG_LINES = 1000 readyDebounceMs = 250 isConnected = false private isReady = false private triggerReady: () => void private deviceTopicReferences: {[deviceId: string]: string} = {} private controlMap: {[controlName: string]: string} = {} repositoryManifest?: IFirmwareRepositoryManifest repositoryUrl = "http://repository.florafi.net" debug = false console = { log: (...args: any[]): void => {this.debug && console.log(...args)}, warn: (...args: any[]): void => {this.debug && console.warn(...args)}, error: (...args: any[]): void => {this.debug && console.error(...args)}, } // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/explicit-module-boundary-types static set(target: any, property: string, value: any): void { const descriptor = Object.getOwnPropertyDescriptor(target, property) if (descriptor == undefined) { Object.defineProperty(target, property, {value, writable: true, enumerable: true}) } else { // this.console.log(property, descriptor) target[property] = value } } // eslint-disable-next-line @typescript-eslint/ban-types static delete(target: object, property: string): void { const descriptor = Object.getOwnPropertyDescriptor(target, property) if (descriptor?.configurable) delete target[property] else target[property] = undefined } constructor() { super() this.triggerReady = debounce(() => { this.isReady = true this.emit("ready") this.console.log("Flora-Fi client is ready.") }, this.readyDebounceMs) } connect(url: string, username?: string, password?: string): FloraFi { if (this.mqtt != undefined) { // this.mqtt.unsubscribe() this.console.log("Closing current MQTT connection...") const currentMqtt = this.mqtt delete this.mqtt currentMqtt.end(true, {disconnect: true}, () => { this.connect(url, username, password) }) return this } this.alerts = [] this.rooms = {} this.devices = {} this.logs = [] const mqtt = connect(url, { username: username, password: password, reconnectPeriod: 1000 * 3 }) this.mqtt = mqtt mqtt.subscribe("florafi/device/+", {qos: 1}) mqtt.on('connect', () => { this.console.log("[mqtt] connected") this.isConnected = true this.emit("connect") this.triggerReady() }) mqtt.on('reconnect', () => { this.isConnected = false this.console.warn("[mqtt] reconnecting") this.emit("reconnect") }) mqtt.on('offline', () => { this.isConnected = false console.error("[mqtt] offline") this.emit("offline") }) mqtt.on('error', (e) => { this.isConnected = false console.error("[mqtt] error:", e) this.emit('error', e) }) mqtt.on('message', (topic, data, packet: IPublishPacket) => { this.processMessage(topic, data, packet) }) return this } subscribe(topic: string): void { if (!this.mqtt) throw new Error("Call 'connect()' first.") this.console.log('Subscribing to', topic) this.mqtt.subscribe(topic) } unsubscribe(topic: string): void { if (!this.mqtt) throw new Error("Call 'connect()' first.") this.mqtt.unsubscribe(topic) } publish(topic: string, payload: string | Buffer, opts: IClientPublishOptions): void { if (!this.mqtt) throw new Error("Call 'connect()' first.") this.mqtt.publish(topic, payload, opts) } disconnect(): void { if (this.mqtt) this.mqtt.end(true) } getRoom(id: string): Room | undefined { return this.rooms[id] } setControl(controlId: string, value: string | number | boolean | null): void { if (!this.mqtt) throw new Error("Call 'connect()' first.") const controlEndpoint = this.controlMap[controlId] if (controlEndpoint == undefined) return console.error(`Unknown control '${controlId}'`) if (value == null) value = "" if (typeof value == "boolean") value = Number(value) if (typeof value == "number") value = String(value) this.console.log(`Publishing control '${controlId}'`, value) this.mqtt.publish(controlEndpoint, value, {qos: 1, retain: false}) } alertDismiss(alert: Alert): void { this.mqtt?.publish(`florafi/room/${alert.roomId}/alert/${alert.type}/${alert.id}`, "", {qos: 1, retain: true}) } deviceForget(device: Device): void { // delete topics pointing to this device for (let [topic, deviceId] of Object.entries(this.deviceTopicReferences)) { if (deviceId == device.id) { this.console.log("Deleting device reference", deviceId, topic) this.mqtt?.publish(topic, "", {retain: true}) delete this.deviceTopicReferences[deviceId] } } this.mqtt?.publish(`florafi/device/${device.id}`, "", {retain: true}) } deviceActivate(device: Device): void { this.console.log(`Activating '${device.name}' (${device.id})`) const data = {settings: {deactivated: false}} this.mqtt?.publish(`homie/${device.id}/$implementation/config/set`, JSON.stringify(data)) } deviceDeactivate(device: Device): void { this.console.log(`Deactivating '${device.name}' (${device.id})`) const data = {settings: {deactivated: true}} this.deviceConfigure(device, data) } deviceReboot(device: Device): void { this.console.log(`Rebooting '${device.name}' (${device.id})`) this.mqtt?.publish(`homie/${device.id}/$implementation/config/set`, '{}') } deviceConfigure(device: Device, config: IHomieConfig): void { this.console.log(`Configuring '${device.name}' (${device.id})`, config) this.mqtt?.publish(`homie/${device.id}/$implementation/config/set`, JSON.stringify(config), {retain: true}) } deviceCheckRepository(device: Device): boolean { const manifest = this.repositoryManifest let repository_channel = device.settings.update_channel || 'stable' // no firmware available if (!manifest || !device.firmware.name || !manifest[device.firmware.name] || !manifest[device.firmware.name][repository_channel]) { delete device.firmwareUpdate return false } device.firmwareUpdate = manifest[device.firmware.name][repository_channel] return true } checkFirmwareRepository(repositoryUrl?: string): Promise { this.repositoryManifest = undefined if (!repositoryUrl) { if (this.repositoryUrl) { repositoryUrl = this.repositoryUrl } else { console.error("missing repositoryUrl") return new Promise((resolve, reject) => {reject()}) } } return axios.get(repositoryUrl + '/manifest.json', {params: {_nocache: Math.random()}}).then(r => { this.repositoryManifest = r.data const summary: FirmwareUpdateSummary = {updated: 0, outdated: 0, unknown: 0} Object.values(this.devices).forEach(d => { if (this.deviceCheckRepository(d)) { d.isUpdated() ? summary.updated += 1 : summary.outdated += 1 } else { summary.unknown += 1 } }) return summary }) } deviceInstallFirmwareUpdate(device: Device): void { if (!this.repositoryUrl) return console.error("Missing florafi.repositoryUrl") const update = device.firmwareUpdate if (!update) { console.error(`Device '${device.name}' (${device.id}) has no update available.`) return } if (!update.binary_path) return console.error("Missing device.firmwareUpdate.binary_path") // data promise if (update.data == undefined) { const url = this.repositoryUrl + "/" + update.binary_path // this.console.log("formware url", url) update.data = axios.get(url, {responseType: 'arraybuffer'}).then(res => { this.console.log("firmware response", res) return res.data }) } // send data via mqtt update.data.then(data => { const topic = `homie/${device.id}/$implementation/ota/firmware/${update.binary_checksum}` this.console.log(`Sending firmware: ${update.name} v${update.version} (${update.binary_checksum}), ${data.byteLength} bytes to ${device.name} (${device.id})`) this.mqtt?.publish(topic, new Buffer(data), {qos: 1}) }) } deviceMoveTo(device: Device, roomId: string): void { this.console.log(`Moving '${device.name}' (${device.id}) to '${roomId}'`) this.mqtt?.publish(`homie/${device.id}/$implementation/config/set`, JSON.stringify({ settings: {garden_room: roomId} })) } roomDisableAlertsUntil(room: Room, until: number): void { this.console.log(`Disabling alerts on ${room.id} until ${until}`) this.mqtt?.publish(`florafi/room/${room.id}/state/alerts/disabled_until`, String(until), {retain: true}) } // deviceUpdateSettings() { } // deviceUpdateWifiConfig() { } // deviceUpdateMqttConfig() { } private installDevice(id: string) { if (this.devices[id]) return this.devices[id] let device = new Device(id, this) FloraFi.set(this.devices, id, device) // this.mqtt?.subscribe(`homie/${id}/$implementation/config`, {qos: 1}) this.emit("install.device", {device}) return device } private installRoom(roomId: string): Room { if (this.rooms[roomId]) return this.rooms[roomId] if (!this.mqtt) throw ("") const room = new Room(roomId, this) FloraFi.set(this.rooms, roomId, room) const eventData = {room} this.emit("install.room", eventData) this.emit(`install.room.${room.id}`, eventData) return room } private processMessage(topic: string, data: Buffer, packet: IPublishPacket) { // this.console.log("[mqtt] %s%s\t%s", packet.retain ? '* ' : '', topic, String(data)) this.emit("message", topic, data, packet) // if (data.length == 0) // this.console.warn("empty message on topic ", topic) const topicParts = topic.split("/") const messageType = topicParts.shift() if (messageType == "florafi") this.processFlorafiMessage(topicParts, data, packet) else if (messageType == "homie") this.processHomieMessage(topicParts, data, packet) else this.console.warn(`Unknown message topic '${topic}'`) // is ready flag if (!this.isReady) { this.triggerReady() } } private processFlorafiMessage(topicParts: string[], data: Buffer, packet: IPublishPacket) { const messageType = topicParts.shift() if (messageType == "room") this.processRoomMessage(topicParts, data, packet) else if (messageType == "device") this.processDeviceDiscoveryMessage(topicParts, data, packet) else this.console.warn(`Unknown florafi message type '${messageType}'`) } private processDeviceDiscoveryMessage(topicParts: string[], data: Buffer, packet: IPublishPacket) { let deviceId = topicParts.shift() if (typeof deviceId == "undefined") return console.error("Invalid device discovery message. Missing device id on topic: ", packet.topic) // forget message if (data.length == 0) { // uninstall if (this.devices[deviceId]) { this.console.log(`Uninstalling device '${deviceId}'`, this.devices[deviceId]) const eventData = {device: this.devices[deviceId]} FloraFi.delete(this.devices, deviceId) this.emit('uninstall.device', eventData) } return } const discoveryData = JSON.parse(String(data)) this.console.log(`Found device '${deviceId}'`, discoveryData) // load device const device = this.installDevice(deviceId) device.isDeactivated = discoveryData.deactivated // install room device.room = discoveryData.room ? this.installRoom(discoveryData.room) : null } private processRoomMessage(topicParts: string[], data: Buffer, packet: IPublishPacket) { // find or create room const roomId = topicParts.shift() if (typeof roomId == "undefined") { console.error("Invalid room message. Missing room id on topic.") return } let room = this.getRoom(roomId) if (!room) { console.error(`Invalid room message: unknown room ${roomId}. (${packet.topic})`) return } // dispatch subtopic const messageType = topicParts.shift() this.console.log("Dispatching room msg:", messageType, packet.topic) switch (messageType) { case "state": this.processRoomStateMessage(room, topicParts, data, packet); break; case "log": this.processRoomLogMessage(room, topicParts, data, packet); break; case "alert": this.processRoomAlertMessage(room, topicParts, data, packet); break; case "notification": this.processRoomNotificationMessage(room, topicParts, data, packet); break; case "control": this.processRoomControlMessage(room, topicParts, data, packet); break; case "device": this.processRoomDeviceMessage(room, topicParts, data, packet); break; case "$name": room.name = String(data) break; default: this.console.warn(`Unknown room message type '${messageType}'`) } } private getRoomComponent(room: Room, componentName: string): Component | null { // resolve const resolvedComponent = components.resolveComponent(componentName) if (!resolvedComponent) return null const componentId = resolvedComponent.name // return existing if (room[componentId]) return room[componentId] as Component // instantiate const ComponentClass = resolvedComponent.class const component = new ComponentClass() component.id = componentId Object.assign(component, locale.component[componentId] || {}) // init props if (!this.legacyComponentSchema) { const state = component['_state'] = {} for (const prop in ComponentClass.SCHEMA) { state[prop] = null const camelCasePropName = camelCase(prop) Object.defineProperty(component, camelCasePropName, { get() { return state[prop] }, set(value) { // console.log("setting control", `${room.id}.${componentName}.${prop}`, value) room.setControl(`${componentId}.${prop}`, value) } }) } } // bind component to room room[componentId] = component return component } private processRoomStateMessage(room: Room, topicParts: string[], data: Buffer, packet: IPublishPacket) { // legacy state shim if (topicParts[0] == "light_intensity") topicParts = ["light", "intensity"] // parse message let componentName = topicParts.shift() if (componentName == undefined) { console.error("Invalid room state message: Missing component name.") return } let propertyName = topicParts.shift() if (propertyName == undefined) { console.error("Invalid room state message: Missing property name. (topic: %s)", packet.topic) return } // find or create component const component = this.getRoomComponent(room, componentName) if (!component) return console.error(`Ignoring room state message: Unknown component '${componentName}'. (topic: %s)`, packet.topic) // inflate data into component property if (this.legacyComponentSchema) { const schema = component.getSchema() const propertyType = schema[propertyName] if (propertyType == undefined) { this.console.warn(`Ignoring room state message: Component '${room.id}.${component.id}' has no '${propertyName}' property.`) return } if (propertyType == "number") { FloraFi.set(component, propertyName, Number(data)) } else if (propertyType == "boolean") { const value = String(data).toUpperCase() FloraFi.set(component, propertyName, value == "ON" || value == "1" || value == "TRUE" || value == "YES") } else if (propertyType == "string") { FloraFi.set(component, propertyName, String(data)) } else { console.error(`Ignoring state message: unknown data type '${propertyType}' on '${component.id}.${propertyName}' `) return } } else { const schema = (component.constructor as ComponentConstructor).SCHEMA const propertyParser = schema[propertyName] if (propertyParser == undefined) { this.console.warn(`Ignoring room state message: Component '${room.id}.${component.id}' has no '${propertyName}' property.`) return } // propertyName = camelCase(propertyName) as string component['_state'][propertyName] = propertyParser(data) // FloraFi.set(component, propertyName, propertyParser(data)) } // emit events const eventData = { room, propertyName, componentName: component.id, value: component[propertyName] } this.emit("state", eventData) this.emit(`state.${room.id}`, eventData) this.emit(`state.${room.id}.${component.id}`, eventData) this.emit(`state.${room.id}.${component.id}.${propertyName}`, eventData) this.emit(`state.*.${component.id}`, eventData) this.emit(`state.*.${component.id}.${propertyName}`, eventData) // this.console.log("room state updated", room) // this.console.log("<< %s =", [room.id, componentName, propertyName].join("."), component[propertyName]) } private processRoomControlMessage(room: Room, topicParts: string[], data: Buffer, packet: IPublishPacket) { // this.console.log("msg:control", topicParts, data, packet) let componentName = topicParts.shift() if (componentName == undefined) return console.error("Invalid room control message: Missing component name.") const resolvedComponent = components.resolveComponent(componentName) if (!resolvedComponent) return console.error(`Ignoring room control message: Unknown component '${componentName}'. (topic: %s)`, packet.topic) componentName = resolvedComponent.name const controlName = topicParts.shift() if (controlName == undefined) return console.error(`Ignoring room control message: Missing control name. (topic: %s)`, packet.topic) const controlId = [room.id, componentName, controlName].join('.') const endpoint = String(data) this.controlMap[controlId] = endpoint // this.console.log("Registering control:", controlId, endpoint) } private processRoomDeviceMessage(room: Room, topicParts: string[], data: Buffer, packet: IPublishPacket) { // this.console.log("[msg:device] %s: %s", packet.topic, String(data)) let componentName = topicParts.shift() if (componentName === undefined) return console.error("Invalid room device message: Missing component name. (topic: %s)", packet.topic) // legacy support, convert 'ipcc' into temperature, humidity, light if (componentName == "ipcc") { this.processRoomDeviceMessage(room, ["temperature"], data, packet) this.processRoomDeviceMessage(room, ["humidity"], data, packet) this.processRoomDeviceMessage(room, ["light"], data, packet) return } const component = this.getRoomComponent(room, componentName) if (!component) return console.error("Invalid room device message: Unresolved component. (topic: %s)", packet.topic) const componentId = component.id as string // update device <-> component link const deviceId = String(data) const device = this.installDevice(deviceId) // delete room component->device link if (data.length == 0) { component.device = null // FloraFi.delete(room.device, componentId) delete this.deviceTopicReferences[packet.topic] FloraFi.set(room.stats, 'componentsTotal', room.stats.componentsTotal - 1) const eventData = {room, componentId} this.emit(`uninstall.component`, eventData) this.emit(`uninstall.component.${room.id}`, eventData) this.emit(`uninstall.component.${room.id}.${componentId}`, eventData) return } else { // bind device to room component if (room.device[componentId] != device) { component.device = device // FloraFi.set(room.device, componentId, device) // room.stats.componentsTotal += 1 FloraFi.set(room.stats, 'componentsTotal', room.stats.componentsTotal + 1) // room.device[componentId] = device // this.console.log("room device", room.device) this.deviceTopicReferences[packet.topic] = device.id const eventData = {room, componentName: componentId, device} this.emit("install.component", eventData) this.emit(`install.component.${room.id}`, eventData) this.emit(`install.component.${room.id}.${componentId}`, eventData) } } // rebuild device component list device.components = room.components.filter(c => c.device?.id == device.id) } private processRoomLogMessage(room: Room, topicParts: string[], data: Buffer, packet: IPublishPacket) { // parse const logLevel = topicParts.shift() if (packet.retain) return console.error(`Ignoring retained log message. (topic: ${packet.topic})`) if (logLevel == undefined) return console.error(`Invalid log message: missing log level. (topic: ${packet.topic})`) if (!(logLevel == 'debug' || logLevel == 'info' || logLevel == 'warning' || logLevel == 'error')) return console.error(`Invalid log message: unkown log level '${logLevel}'. (topic: ${packet.topic})`) let logLine: LogLine const stringData = String(data) if (stringData.substr(0, 2) == '{"') { try { const logData = JSON.parse(stringData) logLine = new LogLine({...logData, roomId: room.id, level: logLevel}) } catch (e) { console.warn(`Error parsing '${room.id}' log: ${stringData}`) return } } else { const deviceId = topicParts.shift() if (deviceId == undefined) return console.error(`Invalid log message: missing device id. (topic: ${packet.topic})`) logLine = new LogLine({ roomId: room.id, level: logLevel, time: new Date().getTime() / 1000 | 0, message: stringData, device: deviceId }) } // store room.lastLog = logLine if (this.MAX_LOG_LINES > 0) { const logs = this.logs if (logs.length == this.MAX_LOG_LINES) logs.shift() logs.push(logLine) } // emit const eventData = {room, logLine} this.emit("log", eventData) this.emit(`log.*.${logLevel}`, eventData) this.emit(`log.${room.id}`, eventData) this.emit(`log.${room.id}.${logLevel}`, eventData) // this.console.log(logLine) } private processRoomAlertMessage(room: Room, topicParts: string[], data: Buffer, packet: IPublishPacket) { // this.console.log("[alert] %s: %s", packet.topic, String(data)) const alertType = topicParts.shift() if (alertType == undefined) return console.error("Invalid alert message: Missing type. (topic: %s)", packet.topic) if (!(alertType == "info" || alertType == "warning" || alertType == "error")) return this.console.warn("Ignoring alert message: Invalid type. (topic: %s)", packet.topic) const alertId = topicParts.shift() if (alertId == undefined) return console.error("Invalid alert message: Missing id. (topic: %s)", packet.topic) // find or create alert let alert = find(this.alerts, (a: Alert) => a.roomId == room.id && a.id == alertId) if (alert == undefined) { alert = new Alert({id: alertId, type: alertType, roomId: room.id}) // this.alerts[alertKey] = alert // FloraFi.set(this.alerts, alertKey, alert) this.alerts.push(alert) } // process message FloraFi.set(alert, 'timestamp', data.length > 0 ? new Date(Number(data) * 1000) : null) FloraFi.set(alert, 'active', alert.timestamp != null) // this.console.log("alert", alert) // room stats const activeAlerts = this.alerts.filter(a => a.roomId == room.id && a.active) room.stats.alertsTotal = activeAlerts.length room.stats.alertsInfo = activeAlerts.filter(a => a.isInfo).length room.stats.alertsWarning = activeAlerts.filter(a => a.isWarning).length room.stats.alertsError = activeAlerts.filter(a => a.isError).length // emit events const eventData = {room, alert} this.emit("alert", eventData) this.emit(`alert.*.${alertType}`, eventData) this.emit(`alert.${room.id}`, eventData) this.emit(`alert.${room.id}.${alertType}`, eventData) } private processRoomNotificationMessage(room: Room, topicParts: string[], data: Buffer, packet: IPublishPacket) { if (packet.retain) return; const notification = new RoomNotification({ time: new Date().getTime() / 1000 | 0, room: room, message: String(data) }) // this.console.log("got notification", notification) const eventData = {room, notification} this.emit(`notification`, eventData) this.emit(`notification.${room.id}`, eventData) } private processHomieMessage(topicParts: string[], data: Buffer, packet: IPublishPacket) { // this.console.log("got Homie message", packet.topic) const deviceId = topicParts.shift() if (deviceId == undefined) return console.error(`Invalid homie message: missing device id.`) const device = this.devices[deviceId] if (device == undefined) return console.error(`Invalid homie message: unknown device. (topic ${packet.topic}).`) // dispatch const subtopic = topicParts.shift() if (subtopic == "$state") this.processDeviceStateMessage(device, data, packet) else if (subtopic == "$stats") this.processDeviceStatsMessage(device, topicParts, data) else if (subtopic == "$localip" || subtopic == "$mac") this.processDeviceNetworkMessage(device, subtopic, data) else if (subtopic == "$implementation" && topicParts[0] == "ota") this.processDeviceOtaMessage(device, topicParts[1], data, packet) else if (subtopic == "$implementation" && topicParts[0] == "config") this.processDeviceConfigMessage(device, data) else if (subtopic == "$fw") this.processDeviceFirmwareMessage(device, topicParts[0], data) else this.console.warn(`Invalid homie message: unknown subtopic '${subtopic}'`, String(data)) } private processDeviceConfigMessage(device: Device, data: Buffer) { let config: IHomieConfig try { config = JSON.parse(String(data)) } catch (e) { console.error(`Error parsing device ${device.id} config. Data: `, String(data)) return } FloraFi.set(device, 'isUnknown', false) if (config.name) FloraFi.set(device, 'name', config.name) if (config.settings?.garden_room) FloraFi.set(device, 'roomId', config.settings.garden_room) if (typeof config.wifi == "object") FloraFi.set(device.network, 'wifi', config.wifi) if (typeof config.mqtt == "object") FloraFi.set(device.network, 'mqtt', config.mqtt) if (typeof config.settings == "object") FloraFi.set(device, 'settings', config.settings) if (typeof config.ota == "object") FloraFi.set(device.ota, 'enabled', config.ota.enabled) if (typeof config.settings?.deactivated == "boolean") FloraFi.set(device, 'isDeactivated', config.settings.deactivated) // if (device.isLoading) { // this.mqtt?.subscribe(`homie/${device.id}/$state`) // this.mqtt?.subscribe(`homie/${device.id}/$stats/+`) // this.mqtt?.subscribe(`homie/${device.id}/$fw/+`) // this.mqtt?.subscribe(`homie/${device.id}/$implementation/ota/status`) // this.mqtt?.subscribe(`homie/${device.id}/$localip`) // this.mqtt?.subscribe(`homie/${device.id}/$mac`) // } // this.console.log("device config", config) } private processDeviceStateMessage(device: Device, data: Buffer, packet: IPublishPacket) { const state = String(data) // this.console.log("Device '%s' state: '%s'", device.id, state) FloraFi.set(device, 'isOnline', state == 'ready') if (device.isLoading) FloraFi.set(device, 'isLoading', false) if (!packet.retain) { const state = device.isOnline ? "online" : "offline" const eventData = {device, state} this.emit(`device_state`, eventData); this.emit(`device_state.${state}`, eventData); this.emit(`device_state.${device.id}`, eventData); } } private processDeviceStatsMessage(device: Device, topicParts: string[], data: Buffer) { const statName = topicParts.shift() if (statName == "uptime") { const uptime = Number(data) const rebooted = device.stats.uptime && uptime < device.stats.uptime // FloraFi.set(device.stats, 'uptime', uptime) device.stats.uptime = uptime if (rebooted) device.onReboot() } else if (statName == "signal") FloraFi.set(device.stats, 'signal', Number(data)) else if (statName == "free_heap") FloraFi.set(device.stats, 'freeHeap', Number(data)) else if (statName == "interval") FloraFi.set(device.stats, 'interval', Number(data)) else console.error(`Invalid device state message: unknown '${statName}' stat.`) } private processDeviceNetworkMessage(device: Device, subtopic: string, data: Buffer) { if (subtopic == "$localip") FloraFi.set(device.network, 'ip', String(data)) else if (subtopic == "$mac") FloraFi.set(device.network, 'mac', String(data)) else console.error(`Invalid device network message: unkown subtopic '${subtopic}'`) } private processDeviceFirmwareMessage(device: Device, subtopic: string, data: Buffer) { if (subtopic == "name") FloraFi.set(device.firmware, 'name', String(data)) else if (subtopic == "version") FloraFi.set(device.firmware, 'version', String(data)) else if (subtopic == "checksum") FloraFi.set(device.firmware, 'checksum', String(data)) else this.console.warn(`Invalid device firmware message: unkown subtopic '${subtopic}'`) } private processDeviceOtaMessage(device: Device, subtopic: string, data: Buffer, packet: IPublishPacket) { if (packet.retain) return; if (subtopic == "status") { const [statusCode, statusMessage] = String(data).split(' ') this.console.log("[ota] '%s' '%s' '%s' '%s'", device.id, String(data), statusCode, statusMessage) if (statusCode) FloraFi.set(device.ota.status, 'code', Number(statusCode)) else return console.error("Error parsing OTA status message:", String(data)) // firmware received, beginning installation FloraFi.set(device.ota.status, 'message', "") if (device.ota.status.code == 202) { FloraFi.set(device.ota.status, 'progress', 1) } // firmware installation finished else if (device.ota.status.code == 200) { FloraFi.set(device.ota.status, 'progress', 100) } // installation progress else if (device.ota.status.code == 206) { const [sent, total] = statusMessage.split('/') if (typeof sent == "string" && typeof total == "string") { FloraFi.set(device.ota.status, 'message', "") FloraFi.set(device.ota.status, 'progress', Math.ceil(Number(sent) / Number(total) * 100)) } else { this.console.warn("Malformed OTA progress message:", statusMessage) } } FloraFi.set(device.ota.status, 'message', statusMessage) FloraFi.set(device.ota.status, 'isError', device.ota.status.code >= 400) const eventData = {device, status: device.ota.status} this.emit('ota.status', eventData) this.emit(`ota.status.${device.id}`, eventData) } // this.console.log("OTA msg", device.id, device.ota) } }