homebridge-mi-flora-care
Version:
This is a homebridge plugin for the Xiaomi Mi Flora / Xiaomi Flower Care devices.
418 lines • 18.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MiFloraCareAccessory = void 0;
const plantSensorService_1 = require("./plantSensorService");
const os_1 = __importDefault(require("os"));
const fakegato_history_1 = __importDefault(require("fakegato-history"));
const miflora_1 = __importDefault(require("miflora"));
const hostname = os_1.default.hostname();
class MiFloraCareAccessory {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.humidityAlertLevel = 0;
this.lowLightAlertLevel = 0;
this.log = log;
this.config = config;
this.api = api;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.PlantSensor = (0, plantSensorService_1.plantSensorService)(this.api);
// extract name from config
this.name = config.name || 'MiFlora';
this.displayName = this.name;
this.deviceId = config.deviceId;
this.interval = typeof config.interval === 'undefined' ? 1800 : config.interval;
this.storedData = {};
// MiFLora scan input
this._opts = {
duration: 30000,
ignoreUnknown: true,
addresses: [this.deviceId.toLowerCase()],
};
if (config.humidityAlertLevel !== null && config.humidityAlertLevel !== undefined) {
this.humidityAlert = true;
this.humidityAlertLevel = config.humidityAlertLevel;
}
else {
this.humidityAlert = false;
}
if (config.lowLightAlertLevel !== null && config.lowLightAlertLevel !== undefined) {
this.lowLightAlert = true;
this.lowLightAlertLevel = config.lowLightAlertLevel;
}
else {
this.lowLightAlert = false;
}
if (config.lowBatteryWarningLevel !== null
&& config.lowBatteryWarningLevel !== undefined
&& typeof config.lowBatteryWarningLevel === 'number') {
this.lowBatteryWarningLevel = config.lowBatteryWarningLevel;
}
else {
this.lowBatteryWarningLevel = 10;
}
// Setup services
this.informationService = this._createInformationService();
this.batteryService = this._createBatteryService();
this.tempService = this._createTemperatureService();
[this.lightService, this.lowLightAlertService] = this._createLightService();
[this.humidityService, this.humidityAlertService] = this._createHumidityService();
this.plantSensorService = this._createPlantService();
this.fakeGatoHistoryService = this._createFakeGatoHistoryService();
this.log.debug('First Scan');
this._refreshInfo();
setInterval(() => {
// Start scanning for updates, these will arrive in the corresponding callbacks
this.log.debug('Interval Scan');
this._refreshInfo();
}, this.interval * 1000);
}
/*
* This method is called directly after creation of this instance.
* It should return all services which should be added to the accessory.
*/
getServices() {
const services = [
this.informationService,
this.batteryService,
this.lightService,
this.tempService,
this.humidityService,
this.plantSensorService,
this.fakeGatoHistoryService,
];
if (this.humidityAlert && this.humidityAlertService) {
services.push(this.humidityAlertService);
}
if (this.lowLightAlert && this.lowLightAlertService) {
services.push(this.lowLightAlertService);
}
return services;
}
/**
* own characteristics and services
*
* @returns Service
*/
_createPlantService() {
const plantSensorService = new this.PlantSensor(this.name);
plantSensorService
.getCharacteristic(this.PlantSensor.SoilMoisture)
.onGet(this.getCurrentMoisture.bind(this));
plantSensorService
.getCharacteristic(this.PlantSensor.SoilFertility)
.onGet(this.getCurrentFertility.bind(this));
return plantSensorService;
}
_createInformationService() {
const informationService = new this.Service.AccessoryInformation();
informationService
.setCharacteristic(this.Characteristic.Manufacturer, this.config.manufacturer || 'Xiaomi')
.setCharacteristic(this.Characteristic.Model, this.config.model || 'Flower Care')
.setCharacteristic(this.Characteristic.SerialNumber, this.config.serial || hostname + '-' + this.name);
informationService
.getCharacteristic(this.Characteristic.FirmwareRevision)
.onGet(this.getFirmwareRevision.bind(this));
return informationService;
}
_createBatteryService() {
const batteryService = new this.Service.Battery(this.name);
batteryService
.getCharacteristic(this.Characteristic.BatteryLevel)
.onGet(this.getBatteryLevel.bind(this));
batteryService
.setCharacteristic(this.Characteristic.ChargingState, this.Characteristic.ChargingState.NOT_CHARGEABLE);
batteryService
.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(this.getStatusLowBattery.bind(this));
return batteryService;
}
_createTemperatureService() {
const tempService = new this.Service.TemperatureSensor(this.name);
tempService
.getCharacteristic(this.Characteristic.CurrentTemperature)
.onGet(this.getCurrentTemperature.bind(this));
tempService
.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(this.getStatusLowBattery.bind(this));
tempService
.getCharacteristic(this.Characteristic.StatusActive)
.onGet(this.getStatusActive.bind(this));
return tempService;
}
_createLightService() {
const lightService = new this.Service.LightSensor(this.name);
lightService
.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel)
.onGet(this.getCurrentAmbientLightLevel.bind(this));
lightService
.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(this.getStatusLowBattery.bind(this));
lightService
.getCharacteristic(this.Characteristic.StatusActive)
.onGet(this.getStatusActive.bind(this));
let lowLightAlertService;
if (this.lowLightAlert) {
lowLightAlertService = new this.Service.ContactSensor(this.name + ' Low Light', 'light');
lowLightAlertService
.getCharacteristic(this.Characteristic.ContactSensorState)
.onGet(this.getStatusLowLight.bind(this));
lowLightAlertService
.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(this.getStatusLowBattery.bind(this));
lowLightAlertService
.getCharacteristic(this.Characteristic.StatusActive)
.onGet(this.getStatusActive.bind(this));
}
else {
lowLightAlertService = null;
}
return [
lightService,
lowLightAlertService,
];
}
_createHumidityService() {
const humidityService = new this.Service.HumiditySensor(this.name);
humidityService
.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)
.onGet(this.getCurrentMoisture.bind(this));
humidityService
.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(this.getStatusLowBattery.bind(this));
humidityService
.getCharacteristic(this.Characteristic.StatusActive)
.onGet(this.getStatusActive.bind(this));
let humidityAlertService;
if (this.humidityAlert) {
humidityAlertService = new this.api.hap.Service.ContactSensor(this.name + ' Low Humidity', 'humidity');
humidityAlertService
.getCharacteristic(this.Characteristic.ContactSensorState)
.onGet(this.getStatusLowMoisture.bind(this));
humidityAlertService
.getCharacteristic(this.Characteristic.StatusLowBattery)
.onGet(this.getStatusLowBattery.bind(this));
humidityAlertService
.getCharacteristic(this.Characteristic.StatusActive)
.onGet(this.getStatusActive.bind(this));
}
else {
humidityAlertService = null;
}
return [
humidityService,
humidityAlertService,
];
}
_createFakeGatoHistoryService() {
const FakeGatoHistory = (0, fakegato_history_1.default)(this.api);
const fakeGatoHistoryService = new FakeGatoHistory('room', this, { storage: 'fs' });
return fakeGatoHistoryService;
}
_updateData({ temperature, lux, moisture, fertility }) {
// Homebridge throws an error if the emitted characteristic value is below 0.0001
if (lux < 0.0001) {
lux = 0.0001;
}
this.log.info('Lux: %s, Temperature: %s, Moisture: %s, Fertility: %s', lux, temperature, moisture, fertility);
this.storedData.data = {
temperature,
lux,
moisture,
fertility,
};
this.fakeGatoHistoryService.addEntry({
time: new Date().getTime() / 1000,
temp: temperature,
humidity: moisture,
});
this.lightService.updateCharacteristic(this.Characteristic.CurrentAmbientLightLevel, lux);
this.lightService.updateCharacteristic(this.Characteristic.StatusActive, true);
this.tempService.updateCharacteristic(this.Characteristic.CurrentTemperature, temperature);
this.tempService.updateCharacteristic(this.Characteristic.StatusActive, true);
this.humidityService.updateCharacteristic(this.Characteristic.CurrentRelativeHumidity, moisture);
this.humidityService.updateCharacteristic(this.Characteristic.StatusActive, true);
if (this.humidityAlert && this.humidityAlertService) {
const alert = moisture <= this.humidityAlertLevel ?
this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
this.Characteristic.ContactSensorState.CONTACT_DETECTED;
this.humidityAlertService.updateCharacteristic(this.Characteristic.ContactSensorState, alert);
this.humidityAlertService.updateCharacteristic(this.Characteristic.StatusActive, true);
}
if (this.lowLightAlert && this.lowLightAlertService) {
const alert = lux <= this.lowLightAlertLevel ?
this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
this.Characteristic.ContactSensorState.CONTACT_DETECTED;
this.lowLightAlertService.updateCharacteristic(this.Characteristic.ContactSensorState, alert);
this.lowLightAlertService.updateCharacteristic(this.Characteristic.StatusActive, true);
}
}
_updateFirmware({ firmware, battery }) {
this.log.info('Firmware: %s, Battery level: %s', firmware, battery);
this.storedData.firmware = {
firmwareVersion: firmware,
batteryLevel: battery,
};
// Update values
this.informationService.updateCharacteristic(this.Characteristic.FirmwareRevision, firmware);
const batteryAlert = battery <= this.lowBatteryWarningLevel ?
this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW :
this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
this.batteryService.updateCharacteristic(this.Characteristic.BatteryLevel, battery);
this.batteryService.updateCharacteristic(this.Characteristic.StatusLowBattery, batteryAlert);
this.lightService.updateCharacteristic(this.Characteristic.StatusLowBattery, batteryAlert);
this.tempService.updateCharacteristic(this.Characteristic.StatusLowBattery, batteryAlert);
this.humidityService.updateCharacteristic(this.Characteristic.StatusLowBattery, batteryAlert);
if (this.humidityAlert && this.humidityAlertService) {
this.humidityAlertService.updateCharacteristic(this.Characteristic.StatusLowBattery, batteryAlert);
}
if (this.lowLightAlert && this.lowLightAlertService) {
this.lowLightAlertService.updateCharacteristic(this.Characteristic.StatusLowBattery, batteryAlert);
}
}
_scan() {
const discover = (address, opts) => {
// try to discover 3 times
return new Promise((resolve) => {
setTimeout(async () => {
try {
this.log.debug(`Discover Start ${address}`);
const allDevices = await miflora_1.default.discover(opts);
const devices = allDevices.filter(device => device.address === address);
resolve(devices);
}
catch (error) {
this.log.error(String(error));
resolve([]);
}
this.log.debug(`Discover End ${address}`);
}, 10 * 1000); // 10 sec
});
};
const scan = async (address, opts) => {
try {
this.log.debug(`Scan Start ${address}`);
let devices = await discover(address, opts);
if (devices.length === 0) {
devices = await discover(address, opts);
}
if (devices.length === 0) {
devices = await discover(address, opts);
}
this.log.debug(`Scan Got ${devices.length}`);
return devices;
}
catch (error) {
this.log.error(String(error));
return [];
}
};
return new Promise((resolveScan) => {
MiFloraCareAccessory._waitScan = MiFloraCareAccessory._waitScan
.then(async () => {
const address = this._opts.addresses[0];
if (this.miFloraDevice) {
this.log.debug(`Scan Cache ${address}`);
resolveScan([this.miFloraDevice]);
}
else {
const devices = await scan(address, this._opts);
if (devices.length) {
this.miFloraDevice = devices[0];
}
resolveScan(devices);
}
await new Promise((resolve) => {
setTimeout(() => {
resolve(0);
}, 15 * 1000); // 15 sec;
});
});
});
}
async _refreshInfo() {
this.log.debug('Mi Flora Care scan...');
const devices = await this._scan();
if (devices.length) {
try {
const device = devices[0];
const data = await device.query();
// {
// address: 'c4:7c:8d:6b:c9:2f',
// type: 'MiFloraMonitor',
// firmwareInfo: { battery: 38, firmware: '3.3.1' },
// sensorValues: { temperature: 21.8, lux: 0, moisture: 41, fertility: 273 }
// }
this._updateData(data.sensorValues);
this._updateFirmware(data.firmwareInfo);
await device.disconnect();
}
catch (e) {
this.log.error(String(e));
}
}
else {
this.log.info('No devide found');
}
}
async getFirmwareRevision() {
return this.storedData.firmware ? this.storedData.firmware.firmwareVersion : '0.0.0';
}
async getBatteryLevel() {
return this.storedData.firmware ? this.storedData.firmware.batteryLevel : 0;
}
async getStatusActive() {
return this.storedData.data ? true : false;
}
async getStatusLowBattery() {
if (this.storedData.firmware) {
return this.storedData.firmware.batteryLevel <= 20 ?
this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW :
this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
}
else {
return this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
}
}
async getStatusLowMoisture() {
if (this.storedData.data) {
return this.storedData.data.moisture <= this.humidityAlertLevel ?
this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
this.Characteristic.ContactSensorState.CONTACT_DETECTED;
}
else {
return this.Characteristic.ContactSensorState.CONTACT_DETECTED;
}
}
async getStatusLowLight() {
if (this.storedData.data) {
return this.storedData.data.lux <= this.lowLightAlertLevel ?
this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
this.Characteristic.ContactSensorState.CONTACT_DETECTED;
}
else {
return this.Characteristic.ContactSensorState.CONTACT_DETECTED;
}
}
async getCurrentAmbientLightLevel() {
return this.storedData.data ? this.storedData.data.lux : 0.0001;
}
async getCurrentTemperature() {
return this.storedData.data ? this.storedData.data.temperature : 0.0001;
}
async getCurrentMoisture() {
return this.storedData.data ? this.storedData.data.moisture : 0.0001;
}
async getCurrentFertility() {
return this.storedData.data ? this.storedData.data.fertility : 0.0001;
}
}
exports.MiFloraCareAccessory = MiFloraCareAccessory;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
MiFloraCareAccessory._waitScan = Promise.resolve();
//# sourceMappingURL=miFloraAccessory.js.map