UNPKG

petkit-feeder-fresh-element

Version:
1,003 lines (883 loc) 42.2 kB
'use strict'; let Service, Characteristic, api; const fs = require('fs'); const packageConfig = require('./package.json') const axios = require('axios'); const deasyncPromise = require('deasync-promise'); const event = require('events'); const format = require('string-format'); const dayjs = require('dayjs'); const pollingtoevent = require('polling-to-event'); const default_headers = Object.freeze({ 'X-Client': 'ios(14.5;iPhone12,3)', 'Accept': '*/*', 'X-Timezone': '-8.0', 'Accept-Language': 'en-US;q=1', 'Accept-Encoding': 'gzip, deflate', 'X-Api-Version': '7.24.0', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'PETKIT/7.24.0 (iPhone; iOS 14.5; Scale/3.00) ', 'X-TimezoneId': 'America/Los_Angeles', 'X-Locale': 'en_US' }) const support_settings = Object.freeze({ 'manualLock' : 'settings.manualLock', // 1 for off, 0 for on 'lightMode' : 'settings.lightMode', }); const global_urls = Object.freeze({ 'north_america':{ 'owndevices': 'http://api.petkt.com/latest/discovery/device_roster', 'deviceState': 'http://api.petkt.com/latest/feeder/devicestate?id={}', 'deviceDetail': 'http://api.petkt.com/latest/feeder/device_detail?id={}', 'saveDailyFeed': 'http://api.petkt.com/latest/feeder/save_dailyfeed?deviceId={}&day={}&time={}&amount={}', 'removeDailyFeed': 'http://api.petkt.com/latest/feeder/remove_dailyfeed?deviceId={}&day={}&id=d{}', 'dailyfeeds': 'http://api.petkt.com/latest/feeder/dailyfeeds?deviceId={}&days={}', 'restoreDailyFeeds': 'http://api.petkt.com/latest/feeder/restore_dailyfeed?deviceId={}&day={}&id=s{}', 'disableDailyFeeds': 'http://api.petkt.com/latest/feeder/remove_dailyfeed?deviceId={}&day={}&id=s{}', 'resetDesiccant': 'http://api.petkt.com/latest/feeder/desiccant_reset?deviceId={}', 'updateSettings': 'http://api.petkt.com/latest/feeder/update?id={}&kv={}', } }); const min_amount = 0; // in meal(same in app) const max_amount = 10; // in meal(same in app) const min_desiccantLeftDays = 0; // in day const max_desiccantLeftDays = 30; // in day const min_batteryLevel = 0; // level(same in app) const max_batteryLevel = 4; // level(same in app) const min_pollint_interval = 60; // in second const max_pollint_interval = 3600; // in second const min_fetch_status_interval = 10; // in second const batteryPersentPerLevel = 100 / max_batteryLevel; module.exports = function(homebridge) { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; api = homebridge; homebridge.registerAccessory('homebridge-petkit-feeder-fresh-element', 'petkit_feeder_fresh_element', petkit_feeder_fresh_element_plugin); } function getTimestamp() { return Math.floor(Date.now() / 1000); } function getDataString() { return dayjs(new Date()).format('YYYYMMDD'); } function getConfigValue(original, default_value) { return (original !== undefined ? original : default_value); } class petkit_feeder_fresh_element_plugin { constructor(log, config) { this.log = log; this.headers = {}; this.lastUpdateTime = 0; this.getDeviceDetailEvent = null; this.poolToEventEmitter = null; this.storagedConfig = { 'mealAmount': 3 }; this.deviceDetailInfo = { 'food' : 0, 'batteryPower': 0, 'batteryStatus': 1, 'desiccantLeftDays' : 0, 'manualLock': 0, 'lightMode': 0, 'meals': {} }; this.log('begin to initialize petkit feeder fresh element.'); // location if (!config['location'] || !global_urls[config['location']]) { this.log.error('wrong value in config.json file: location.'); return; } this.location = config['location']; this.urls = global_urls[config['location']]; // http request headers if (config['headers'] === undefined) { this.log.error('missing field in config.json file: headers.'); return; } this.convertHeadersetFormat(config['headers']); if (!this.headers) { return; } // device id && device detail info this.deviceId = config['deviceId']; const devices = this.praseGetDeviceResult(this.dePromise(this.http_getOwnDevice())); if (!devices) { return; } else if (this.deviceId !== undefined && devices != this.deviceId) { this.log.warn('found you just ownd one feeder with deviceId: '+ devices); this.log.warn('which is not the same with the deviceId you set: '+ this.deviceId); this.log.warn('use '+ devices + ' instead of ' + this.deviceId); this.deviceId = devices; } else { this.log('found you just ownd one feeder with deviceId: '+ devices); this.deviceId = devices; } this.storagedConfig = this.readStoragedConfigFromFile(); // device information settings this.name = getConfigValue(config['name'], 'PetkitFeeder'); this.serialNumber = getConfigValue(config['sn'], 'PetkitFeeder'); this.firmware = getConfigValue(config['firmware'], getConfigValue(packageConfig['version'], '1.0.0')); this.manufacturer = getConfigValue(config['manufacturer'], 'Petkit'); this.model = getConfigValue(config['model'], 'Petkit feeder fresh element'); this.autoDeviceInfo = getConfigValue(config['autoDeviceInfo'], false); if (this.autoDeviceInfo && this.dePromise(this.http_getDeviceInfo())) { this.name = this.deviceDetailInfo['name'] || this.name; this.serialNumber = this.deviceDetailInfo['sn'] || this.serialNumber; this.firmware = this.deviceDetailInfo['firmware'] || this.firmware; this.headers['X-Timezone'] = this.deviceDetailInfo['timezone'] || this.headers['X-Timezone']; this.headers['X-TimezoneId'] = this.deviceDetailInfo['locale'] || this.headers['X-Timezone']; } this.replaceHeadersetWithDefault(); // meal, same as petkit app unit. one share stands for 5g or 1/20 cup, ten meal most; this.mealAmount = getConfigValue(this.storagedConfig['mealAmount'], 3); if (this.mealAmount > max_amount) { this.log('mealAmount should not greater than ' + max_amount + ', use ' + max_amount + ' instead'); this.mealAmount = max_amount; } else if (this.mealAmount < min_amount) { this.log('mealAmount should not less than ' + min_amount + ', use ' + min_amount + ' instead'); this.mealAmount = min_amount; } // device desiccant settings this.enable_desiccant = getConfigValue(config['enable_desiccant'], false); this.enable_autoreset_desiccant = getConfigValue(config['enable_autoreset_desiccant'], false); this.alert_desiccant_threshold = getConfigValue(config['alert_desiccant_threshold'], 7); this.reset_desiccant_threshold = getConfigValue(config['reset_desiccant_threshold'], 5); // polling settings this.enable_polling = getConfigValue(config['enable_polling'], true); this.polling_interval = getConfigValue(config['polling_interval'], min_pollint_interval); if (this.polling_interval > max_pollint_interval) { this.log('mealAmount should not greater than ' + max_pollint_interval + ', use ' + max_pollint_interval + ' instead'); this.mealAmount = max_pollint_interval; } else if (this.polling_interval < min_pollint_interval) { this.log('mealAmount should not less than ' + min_pollint_interval + ', use ' + min_pollint_interval + ' instead'); this.mealAmount = min_pollint_interval; } // service names this.service_names = { 'DropMeal': getConfigValue(config['DropMeal_name'], 'DropMeal'), 'MealAmount': getConfigValue(config['MealAmount_name'], 'MealAmount'), 'FoodStorage': getConfigValue(config['FoodStorage_name'], (this.reverse_foodStorage_indicator ? 'FoodStorage_Empty': 'FoodStorage')), 'DesiccantLevel': getConfigValue(config['DesiccantLevel_name'], 'DesiccantLevel'), 'ManualLock': getConfigValue(config['ManualLock_name'], 'ManualLock'), 'LightMode': getConfigValue(config['LightMode_name'], 'LightMode'), 'Battery': getConfigValue(config['Battery_name'], 'Battery') }; // other settings this.enable_manualLock = getConfigValue(config['enable_manualLock'], false); this.enable_lightMode = getConfigValue(config['enable_lightMode'], false); this.reverse_foodStorage_indicator = getConfigValue(config['reverse_foodStorage_indicator'], false); this.fast_response = getConfigValue(config['fast_response'], false); this.log('petkit feeder loaded successfully.'); } getServices() { this.log.debug('begin to initialize homebridge service.'); var services = []; var service_name = null; // meal drop service service_name = this.service_names['DropMeal']; this.drop_meal_service = new Service.Switch(service_name, service_name); this.drop_meal_service.getCharacteristic(Characteristic.On) .on('get', (callback) => callback(null, 0)) .on('set', this.hb_dropMeal_set.bind(this)); services.push(this.drop_meal_service); // meal amount setting service_name = this.service_names['MealAmount']; this.meal_amount_service = new Service.Fan(service_name, service_name); this.meal_amount_service.getCharacteristic(Characteristic.On) .on('get', (callback) => callback(null, this.mealAmount != 0)); this.meal_amount_service.getCharacteristic(Characteristic.RotationSpeed) .on('get', (callback) => callback(null, this.mealAmount)) .on('set', this.hb_mealAmount_set.bind(this)) .setProps({ minValue: min_amount, maxValue: max_amount, minStep: 1 }); services.push(this.meal_amount_service); // food storage indicator service_name = this.service_names['FoodStorage']; this.food_storage_service = new Service.OccupancySensor(service_name, service_name); this.food_storage_service.setCharacteristic(Characteristic.OccupancyDetected, this.deviceDetailInfo['food']) this.food_storage_service.getCharacteristic(Characteristic.OccupancyDetected) .on('get', this.hb_foodStorageStatus_get.bind(this)); services.push(this.food_storage_service); // desiccant left days if (this.enable_desiccant) { service_name = this.service_names['DesiccantLevel']; this.desiccant_level_service = new Service.FilterMaintenance(service_name, service_name); this.desiccant_level_service.setCharacteristic(Characteristic.FilterChangeIndication, (this.deviceDetailInfo['desiccantLeftDays'] < this.alert_desiccant_threshold ? 1 : 0)); this.desiccant_level_service.getCharacteristic(Characteristic.FilterChangeIndication) .on('get', this.hb_desiccantIndicator_get.bind(this)); this.desiccant_level_service.setCharacteristic(Characteristic.FilterLifeLevel, this.deviceDetailInfo['desiccantLeftDays']); this.desiccant_level_service.getCharacteristic(Characteristic.FilterLifeLevel) .on('get', this.hb_desiccantLeftDays_get.bind(this)) .setProps({ minValue: min_desiccantLeftDays, maxValue: max_desiccantLeftDays, minStep: 1 }); this.desiccant_level_service.getCharacteristic(Characteristic.ResetFilterIndication) .on('set', this.hb_desiccantLeftDays_reset.bind(this)) services.push(this.desiccant_level_service); } // manualLock setting if (this.enable_manualLock) { service_name = this.service_names['ManualLock']; this.manualLock_service = new Service.Switch(service_name, service_name); this.manualLock_service.setCharacteristic(Characteristic.On, this.deviceDetailInfo['manualLock']); this.manualLock_service.getCharacteristic(Characteristic.On) .on('get', this.hb_manualLockStatus_get.bind(this)) .on('set', this.hb_manualLockStatus_set.bind(this)); services.push(this.manualLock_service); } // lightMode setting if (this.enable_lightMode) { service_name = this.service_names['LightMode']; this.lightMode_service = new Service.Switch(service_name, service_name); this.lightMode_service.setCharacteristic(Characteristic.On, this.deviceDetailInfo['manualLock']); this.lightMode_service.getCharacteristic(Characteristic.On) .on('get', this.hb_lightModeStatus_get.bind(this)) .on('set', this.hb_lightModeStatus_set.bind(this)); services.push(this.lightMode_service); } // battery status service_name = this.service_names['Battery']; this.battery_status_service = new Service.BatteryService(service_name, service_name); this.battery_status_service.setCharacteristic(Characteristic.BatteryLevel, this.deviceDetailInfo['batteryPower']); this.battery_status_service.getCharacteristic(Characteristic.BatteryLevel) .on('get', this.hb_deviceBatteryLevel_get.bind(this)); this.battery_status_service.setCharacteristic(Characteristic.ChargingState, (this.deviceDetailInfo['batteryStatus'] == 0 ? Characteristic.ChargingState.CHARGING : Characteristic.ChargingState.NOT_CHARGING)); this.battery_status_service.getCharacteristic(Characteristic.ChargingState) .on('get', this.hb_deviceChargingState_get.bind(this)); this.battery_status_service.setCharacteristic(Characteristic.StatusLowBattery, (this.deviceDetailInfo['batteryPower'] <= 50 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL)); this.battery_status_service.getCharacteristic(Characteristic.StatusLowBattery) .on('get', this.hb_deviceStatusLowBattery_get.bind(this)); services.push(this.battery_status_service); // divice information this.info_service = new Service.AccessoryInformation(); this.info_service .setCharacteristic(Characteristic.Identify, this.deviceId) .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) .setCharacteristic(Characteristic.Model, this.model) .setCharacteristic(Characteristic.SerialNumber, this.serialNumber) // infomation below changed from petkit app require a homebridge reboot to take effect. .setCharacteristic(Characteristic.Name, this.name) .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); services.push(this.info_service); // polling this.setupPolling(); this.log.debug('homebridge service initialize success.'); return services; } readStoragedConfigFromFile(callback = null) { var result = {}; try { var parse_rawdata = (rawdata) => { result = JSON.parse(rawdata); if (result[this.deviceId] !== undefined) { result = result[this.deviceId]; } else { result = {}; } return result; }; if (this.deviceId) { const filePath = api.user.storagePath() + '/petkit_feeder_fresh_element.json'; if (callback) { if (!fs.existsSync(filePath)) callback({}); fs.readFile(filePath, (error, rawdata) => { if (error) { this.log.error('readstoragedConfigFromFile failed: ' + error); } else { result = parse_rawdata(rawdata); } }); } else { if (!fs.existsSync(filePath)) return {}; const rawdata = fs.readFileSync(filePath); result = parse_rawdata(rawdata); } } } catch (error) { this.log.error('readstoragedConfigFromFile failed: ' + error); } finally { if (callback) { callback(result); } else { return result; } } } saveStoragedConfigToFile(callback = null) { var result = false; try { if (this.deviceId) { const filePath = api.user.storagePath() + '/petkit_feeder_fresh_element.json'; var data = {}; data[this.deviceId] = this.storagedConfig; const rawdata = JSON.stringify(data); if (callback) { fs.writeFile(filePath, rawdata, (err) => { if (!err) { result = true; } else { this.log.error('saveStoragedConfigToFile failed: ' + err); } }); } else { fs.writeFileSync(filePath, rawdata); result = true; } } } catch (error) { this.log.warn('saveStoragedConfigToFile failed: ' + error); } finally { if (callback) { callback(result); } else { return result; } } } setupPolling() { if (this.enable_polling) { if (this.polling_interval < min_pollint_interval) { this.log.warn('polling interval should greater than ' + min_pollint_interval + '(' + min_pollint_interval / 60 +' min), change to ' + min_pollint_interval + '.'); this.polling_interval = min_pollint_interval; } else if (this.polling_interval > max_pollint_interval) { this.log.warn('polling interval should less than ' + max_pollint_interval + '(' + max_pollint_interval / 60 +' min), change to ' + max_pollint_interval + '.'); this.polling_interval = max_pollint_interval; } const polling_options = { longpolling: true, interval: this.polling_interval * 1000, longpollEventName: 'deviceStatusUpdatePoll' }; setTimeout(() => { this.poolToEventEmitter = pollingtoevent((done) => { this.log('polling start...'); this.http_getDeviceDetailStatus() .then((result) => { done(null, result); this.log('polling end...'); }).catch((error) => {}); }, polling_options); this.poolToEventEmitter.on('deviceStatusUpdatePoll', (result) => { this.uploadStatusToHomebridge(); }); }, this.polling_interval * 1000) } } dePromise(promise) { var result = undefined; try { result = deasyncPromise(promise); } catch(err) { this.log('dePromise error: ' + err); } finally { return result; } } convertHeadersetFormat(config_headers) { if (!config_headers) { return false; } config_headers.forEach((header, index) => { this.headers[header.key] = header.value; }); if (this.headers['X-Session'] === undefined) { this.log.error('missing field in config.json file: headers.X-Session.'); return false; } if (this.headers['X-Session'] !== this.headers['F-Session']) { this.log.debug('header set X-Session should equal to header set F-Session, replace F-Session.'); this.headers['F-Session'] = this.headers['X-Session']; } return true; } replaceHeadersetWithDefault() { Object.keys(default_headers).forEach((key) => { if (this.headers[key] === undefined) { this.log.debug('missing header set: "' + key + '", using "' + default_headers[key] + '" instead.'); this.headers[key] = default_headers[key]; } }); } praseGetDeviceResult(jsonObj) { if (!jsonObj) { this.log.error('praseGetDeviceResult error: jsonObj is nothing.'); return false; } const jsonStr = JSON.stringify(jsonObj); this.log.debug(jsonStr); if (jsonObj.hasOwnProperty('error')) { this.log.error('server reply an error: ' + JSON.stringify(jsonObj)); this.log.error('you may need to check your X-Session and other header configure'); return false; } if (!jsonObj.hasOwnProperty('result')) { this.log.error('JSON.parse error with:' + jsonStr); return false; } if (!jsonObj.result.hasOwnProperty('devices')) { this.log.error('JSON.parse error with:' + jsonStr); return false; } if (jsonObj.result.devices.length === 0) { this.log.error('seems you\'re not owned a device.'); return false; } var devices = []; jsonObj.result.devices.forEach((item, index) => { if (item.type == 'FeederMini' && item.data) { devices.push(item.data); } }); if (devices.length === 0) { this.log.error('seems you does not owned a Petkit feeder, this plugin only works for Petkit feeder, sorry.'); return false; } else if (devices.length === 1) { this.log.debug(JSON.stringify(devices[0])); return devices[0].id; } else { let match_device = devices.find(device => device.id == this.deviceId); if (undefined === match_device) { const devicesIds = devices.map((device) => { return { 'id': device.id, 'name': device.name }; }); this.log.error('seems that you ownd more than one feeder, but the device id you set is not here.'); this.log.error('do you mean one of this: ' + JSON.stringify(devicesIds)); return false; } return match_device.id; } } praseGetDeviceDetailInfo(jsonObj) { if (jsonObj === undefined) { this.log.error('praseGetDeviceDetailInfo error: jsonObj is nothing.'); return false; } const jsonStr = JSON.stringify(jsonObj); this.log.debug(jsonStr); if (jsonObj.hasOwnProperty('error')) { this.log.error('server reply an error: ' + jsonStr); this.log.error('you may need to check your X-Session and other header configure'); return false; } if (this.deviceDetailInfo['name'] === undefined && jsonObj['name'] !== undefined) this.deviceDetailInfo['name'] = jsonObj['name']; if (this.deviceDetailInfo['sn'] === undefined && jsonObj['sn'] !== undefined) this.deviceDetailInfo['sn'] = jsonObj['sn']; if (this.deviceDetailInfo['firmware'] === undefined && jsonObj['firmware'] !== undefined) this.deviceDetailInfo['firmware'] = jsonObj['firmware']; if (this.deviceDetailInfo['timezone'] === undefined && jsonObj['timezone'] !== undefined) this.deviceDetailInfo['timezone'] = jsonObj['timezone']; if (this.deviceDetailInfo['locale'] === undefined && jsonObj['locale'] !== undefined) this.deviceDetailInfo['locale'] = jsonObj['locale']; if (jsonObj['state']) { const state = jsonObj['state']; // 1 for statue ok, 0 for empty if (state['food'] !== undefined) this.deviceDetailInfo['food'] = state['food'] ? 1 : 0; this.log.debug('device food storage status is: ' + (this.deviceDetailInfo['food'] ? 'Ok' : 'Empty')); if (state['batteryPower'] !== undefined) this.deviceDetailInfo['batteryPower'] = state['batteryPower'] * batteryPersentPerLevel; this.log.debug('device battery level is: ' + this.deviceDetailInfo['batteryPower']); // 0 for charging mode, 1 for battery mode if (state['batteryStatus'] !== undefined) this.deviceDetailInfo['batteryStatus'] = state['batteryStatus']; this.log.debug('device battery status is: ' + (this.deviceDetailInfo['batteryStatus'] ? 'not charging' : 'charging')); if (state['desiccantLeftDays'] !== undefined) this.deviceDetailInfo['desiccantLeftDays'] = state['desiccantLeftDays']; this.log.debug('device desiccant remain: ' + (this.deviceDetailInfo['desiccantLeftDays'] + ' day(s)')); } if (jsonObj['settings']) { const settings = jsonObj['settings']; // on for off, off for on, same behavior with Petkit app. if (settings['manualLock'] !== undefined) this.deviceDetailInfo['manualLock'] = settings['manualLock'] ? 0 : 1; this.log.debug('device manual lock status is: ' + (this.deviceDetailInfo['manualLock'] ? 'locked' : 'unlocked')); // 1 for lignt on, 0 for light off if (settings['lightMode'] !== undefined) this.deviceDetailInfo['lightMode'] = settings['lightMode'] ? 1 : 0; this.log.debug('device light status is: ' + (this.deviceDetailInfo['lightMode'] ? 'on' : 'off')); } return true; } praseUpdateDeviceSettingsResult(jsonObj) { if (!jsonObj) { this.log.error('praseUpdateDeviceSettingsResult error: jsonObj is nothing.'); return false; } const jsonStr = JSON.stringify(jsonObj); this.log.debug(jsonStr); if (jsonObj.hasOwnProperty('error')) { this.log.error('server reply an error: ' + jsonStr); this.log.error('you may need to check your X-Session and other header configure'); return false; } if (!jsonObj.hasOwnProperty('result')) { this.log.error('JSON.parse error with:' + jsonStr); return false; } return (jsonObj.result == 'success'); } praseSaveDailyFeedResult(jsonObj) { if (!jsonObj) { this.log.error('praseSaveDailyFeedResult error: jsonObj is nothing.'); return false; } const jsonStr = JSON.stringify(jsonObj); this.log.debug(jsonStr); if (jsonObj.hasOwnProperty('error')) { this.log.error('server reply an error: ' + jsonStr); this.log.error('you may need to check your X-Session and other header configure'); return false; } if (!jsonObj.hasOwnProperty('result')) { this.log.error('JSON.parse error with:' + jsonStr); return false; } if (jsonObj.result.isExecuted == 1) { return true; } return false; } notifyHomebridgeInfoUpdated() { try { // if desiccantLeftDays less than {reset_desiccant_threshold} day, auto reset it. if (this.enable_desiccant) { if (this.enable_autoreset_desiccant) { if (this.deviceDetailInfo.desiccantLeftDays < this.reset_desiccant_threshold) { this.log.debug('desiccant only ' + this.deviceDetailInfo.desiccantLeftDays + 'days left, reset it.'); this.hb_desiccantLeftDays_reset(null); } else { this.log.debug('desiccant has '+ this.deviceDetailInfo.desiccantLeftDays +' days left, no need to reset.'); } } else { this.log.debug('desiccant auto reset function is disabled.'); } } } catch(err) { this.log('notifyHomebridgeInfoUpdated error: ' + err); } finally { } } http_post(url) { const options = { url: url, method: 'POST', headers: this.headers }; return new Promise((resolve) => { var result = false; axios.request(options) .then((response) => { if (response.status != 200) { const error = 'post request success, but received a invalid response code: ' + response.status; this.log.error(error); } else { this.log.debug('post request success') result = response.data; } }) .catch((error) => { this.log.error('post request failed: ' + error); }) .then(() => { resolve(result); }); }); } http_getDeviceInfo() { var result = false; return new Promise((resolve) => { this.http_post(format(this.urls.deviceDetail, this.deviceId)) .then((data) => { if (data) { if (this.praseGetDeviceDetailInfo(data['result'])) { this.log.debug('successfully retrieved device infomation from server.'); result = true; } } }) .catch((error) => { this.log.error("http_getDeviceInfo failed: " + error); }) .then(() => { resolve(result); }); }); } http_getDeviceDailyFeeds() { var result = false; return new Promise((resolve) => { const date = getDataString(); this.http_post(format(this.urls.dailyfeeds, this.deviceId, date)) .then((data) => { if (data && data['result']) { this.deviceDetailInfo['meals'] = data['result']; this.log.debug('successfully retrieved meals infomation from server.'); result = true; } }) .catch((error) => { this.log.error("http_getDeviceDailyFeeds failed: " + error); }) .then(() => { resolve(result); }); }); } http_getOwnDevice() { return this.http_post(this.urls.owndevices); } http_getDeviceState() { // { // "result": { // "batteryPower":4,"batteryStatus":0,"desiccantLeftDays":6, // "errorPriority":0,"feeding":0,"food":1,"ota":0,"overall":1, // "pim":1,"runtime":49677,"wifi":{ // "bssid":"xxxxxxxxxxxx","rsq":-37,"ssid":"xxxxxxxxxx" // } // } // } return this.http_post(format(this.urls.deviceState, this.deviceId)); } http_getDeviceDetailStatus() { return new Promise((resolve) => { const currentTimestamp = getTimestamp(); var getDeviceDetailResult = {deviceInfo:false}; if (currentTimestamp - this.lastUpdateTime > min_fetch_status_interval && this.getDeviceDetailEvent === null) { this.getDeviceDetailEvent = new event.EventEmitter(); this.getDeviceDetailEvent.setMaxListeners(0); Promise.all([ this.http_getDeviceInfo(), // this.http_getDeviceDailyFeeds() ]).then((results) => { getDeviceDetailResult.deviceInfo = results[0]; // device info // getDeviceDetailResult.meals = results[1]; // meals info }) .catch((error) => { this.log.error("http_getDeviceDetail failed: " + error); }) .then(() => { this.lastUpdateTime = currentTimestamp; this.getDeviceDetailEvent.emit('finished', getDeviceDetailResult); this.getDeviceDetailEvent = null; resolve(getDeviceDetailResult); setTimeout(this.notifyHomebridgeInfoUpdated.bind(this), 200); }); } else { this.log.debug('too close to last update time, pass'); resolve(false); } }); } // date:20200920、time: 68400(-1 stand for current)、amount in app unit,1 for 5g, 10 is max(50g) async http_saveDailyFeed(amount, time) { const date = getDataString(); return await this.http_post(format(this.urls.saveDailyFeed, this.deviceId, date, time, amount * 5)); } // key see support_settings. async http_updateDeviceSettings(key, value) { var data = {}; if (support_settings[key]) { data[support_settings[key]] = value; return await this.http_post(format(this.urls.updateSettings, this.deviceId, JSON.stringify(data))); } else { this.log.warn('unsupport setting: ' + key); return false; } } async http_resetDesiccant() { return await this.http_post(format(this.urls.resetDesiccant, this.deviceId)); } uploadStatusToHomebridge() { var status = (this.reverse_foodStorage_indicator ? !this.deviceDetailInfo['food'] : this.deviceDetailInfo['food']); this.food_storage_service.setCharacteristic(Characteristic.OccupancyDetected, status); if (this.enable_desiccant) { status = this.deviceDetailInfo['desiccantLeftDays'] < this.alert_desiccant_threshold ? Characteristic.FilterChangeIndication.CHANGE_FILTER : Characteristic.FilterChangeIndication.FILTER_OK; this.desiccant_level_service.setCharacteristic(Characteristic.FilterChangeIndication, status); } } updataDeviceDetail() { return new Promise((resolve) => { this.http_getDeviceDetailStatus() .then((result) => { if (result) this.uploadStatusToHomebridge(); }) .catch((error) => { this.log.error("updataDeviceDetail failed: " + error); }) .then(resolve); }); } waitForSignal(sig, callback) { if (sig) { const callbackHandler = (results) => { callback(results); sig.removeListener('finished', callbackHandler); } sig.addListener('finished', callbackHandler); return true; } return false; } async hb_handle_get(caller, callback) { this.log.debug('hb_handle_get: ' + caller); if (!this.waitForSignal(this.getDeviceDetailEvent, callback)) { this.updataDeviceDetail() .then(callback) .catch((error) => { this.log.error(caller + ' error: ' + error); }) .then(() => {}); } } hb_handle_set_deviceSettings(settingName, status, callback = null) { this.log.debug('set ' + settingName + ' to: ' + status); var result = false; this.http_updateDeviceSettings(settingName, status) .then((data) => { if (!data) { this.log.error('failed to commuciate with server.'); } else if (this.praseUpdateDeviceSettingsResult(data)) { result = true; this.deviceDetailInfo[settingName] = status; } }).catch((error) => { this.log.error(error); }).then(() => { if (callback) callback(result); if (result) { this.log('set ' + settingName + ' to: ' + status + ', success'); } else { this.log.warn('set ' + settingName + ' to: ' + status + ', failed'); } this.updataDeviceDetail(); }); } hb_mealAmount_set(value, callback) { if (this.fast_response) callback(null); this.mealAmount = value; this.storagedConfig['mealAmount'] = value; this.log('set meal amount to ' + value); this.saveStoragedConfigToFile((this.fast_response ? null : callback)); } hb_dropMeal_set(value, callback) { if (this.fast_response) callback(null); this.log.debug('hb_dropMeal_set'); if (value) { if (this.mealAmount) { this.log('drop food:' + this.mealAmount + ' meal(s)'); var result = false; this.http_saveDailyFeed(this.mealAmount, -1) .then((data) => { if (!data) { this.log.error('failed to commuciate with server.'); } else { result = this.praseSaveDailyFeedResult(data); this.log('food drop result: ' + result ? 'success' : 'failed'); } }) .catch((error) => { this.log.error('food drop failed: ' + error); }) .then(() => { if (!this.fast_response) callback(null); }); } else { this.log('drop food with zero amount, pass.'); } setTimeout(() => { this.drop_meal_service.setCharacteristic(Characteristic.On, false); }, 200); } this.updataDeviceDetail(); } hb_desiccantIndicator_get(callback) { this.hb_handle_get('hb_desiccantIndicator_get', (results) => { const status = (this.deviceDetailInfo['desiccantLeftDays'] < this.alert_desiccant_threshold ? 1 : 0); callback(null, status); }); } hb_desiccantLeftDays_get(callback) { this.hb_handle_get('hb_desiccantLeftDays_get', (results) => { callback(null, this.deviceDetailInfo['desiccantLeftDays']); }); } // reset Desiccant Left Days hb_desiccantLeftDays_reset(callback) { if (this.fast_response && callback) {callback(null);} this.log.debug('hb_desiccantLeftDays_reset'); this.http_resetDesiccant() .then((data) => { if (data && data['result']) { this.deviceDetailInfo['desiccantLeftDays'] = data['result']; this.log('reset desiccant left days success, left days reset to ' + data['result'] + ' days'); } else { this.log('reset desiccant left days with a unrecognized return.'); } }) .catch((error) => { this.log.error('reset desiccant left days failed: ' + error); }) .then(() => { if (!this.fast_response && callback) callback(null); }); } hb_foodStorageStatus_get(callback) { this.hb_handle_get('hb_foodStorageStatus_get', (results) => { callback(null, (this.reverse_foodStorage_indicator ? !this.deviceDetailInfo['food'] : this.deviceDetailInfo['food'])); }); } hb_manualLockStatus_get(callback) { this.hb_handle_get('hb_manualLockStatus_get', (results) => { callback(null, this.deviceDetailInfo['manualLock']); }); } hb_manualLockStatus_set(value, callback) { if (this.fast_response) callback(null); this.hb_handle_set_deviceSettings('manualLock', (value ? 0 : 1), (result) => { if (!this.fast_response) callback(null); }); } hb_lightModeStatus_get(callback) { this.hb_handle_get('hb_lightModeStatus_get', (results) => { const status = this.deviceDetailInfo['lightMode']; callback(null, status); }); } hb_lightModeStatus_set(value, callback) { if (this.fast_response) callback(null); this.hb_handle_set_deviceSettings('lightMode', value, (result) => { if (!this.fast_response) callback(null); }); } hb_deviceBatteryLevel_get(callback) { this.hb_handle_get('hb_deviceBatteryLevel_get', (results) => { callback(null, this.deviceDetailInfo['batteryPower']); }); } hb_deviceChargingState_get(callback) { this.hb_handle_get('hb_deviceChargingState_get', (results) => { const status = (this.deviceDetailInfo['batteryStatus'] == 0 ? Characteristic.ChargingState.CHARGING : Characteristic.ChargingState.NOT_CHARGING); callback(null, status); }); } hb_deviceStatusLowBattery_get(callback) { this.hb_handle_get('hb_deviceStatusLowBattery_get', (results) => { var status = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; if (this.deviceDetailInfo['batteryPower'] < 50) { status = Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } callback(null, status); }); } }