UNPKG

homebridge-petkit-feeder-mini-battery

Version:

control your petkit feeder mini to feed your lovely pet. battery mode, it can be used but ten minutes later

1,043 lines (926 loc) 62.7 kB
'use strict'; let PlatformAccessory, Accessory, Service, Characteristic, UUIDGen; const format = require('string-format'); const axios = require('axios'); const dayjs = require('dayjs'); const pollingtoevent = require('polling-to-event'); const logUtil = require('./utils/log'); const configUtil = require('./utils/config'); const pluginName = 'homebridge-petkit-feeder-mini'; const platformName = 'petkit_feeder_mini'; const globalVariables = Object.freeze({ 'support_device_type': [ // valid petkit feeder device type 'Feeder', // Petkit Feeder Element 'FeederMini' // Petkit Feeder Mini ], 'support_settings': { 'manualLock' : 'settings.manualLock', // 1 for off, 0 for on 'lightMode' : 'settings.lightMode', }, 'default_headers': { 'X-Client': 'ios(14.0;iPhone12,3)', 'Accept': '*/*', 'X-Timezone': '0.0', 'Accept-Language': 'en-US;q=1, zh-Hans-US;q=0.9', 'Accept-Encoding': 'gzip, deflate', 'X-Api-Version': '7.18.1', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'PETKIT/7.18.1 (iPhone; iOS 14.0; Scale/3.00)', 'X-TimezoneId': 'Asia/Shanghai', 'X-Locale': 'en_US' }, 'default_http_options': { 'method': 'POST', 'timeout': 5000, 'responseType': 'json', 'retry' : { 'enabled': true, 'max_retry': 3, // http request retry count } }, 'global_urls': { 'Feeder': { 'cn': { 'owndevices': 'http://api.petkit.cn/6/discovery/device_roster', 'deviceState': 'http://api.petkit.cn/6/feeder/devicestate?id={}', 'deviceDetailInfo': 'http://api.petkit.cn/6/feeder/device_detail?id={}', 'saveDailyFeed': 'http://api.petkit.cn/6/feeder/save_dailyfeed?deviceId={}&day={}&time={}&amount={}', 'removeDailyFeed': 'http://api.petkit.cn/6/feeder/remove_dailyfeed?deviceId={}&day={}&id=d{}', 'dailyfeeds': 'http://api.petkit.cn/6/feeder/dailyfeeds?deviceId={}&days={}', 'restoreDailyFeeds': 'http://api.petkit.cn/6/feeder/restore_dailyfeed?deviceId={}&day={}&id=s{}', 'disableDailyFeeds': 'http://api.petkit.cn/6/feeder/remove_dailyfeed?deviceId={}&day={}&id=s{}', 'resetDesiccant': 'http://api.petkit.cn/6/feeder/desiccant_reset?deviceId={}', 'updateSettings': 'http://api.petkit.cn/6/feeder/update?id={}&kv={}', }, 'asia':{ 'owndevices': 'http://api.petktasia.com/latest/discovery/device_roster', 'deviceState': 'http://api.petktasia.com/latest/feeder/devicestate?id={}', 'deviceDetailInfo': 'http://api.petktasia.com/latest/feeder/device_detail?id={}', 'saveDailyFeed': 'http://api.petktasia.com/latest/feeder/save_dailyfeed?deviceId={}&day={}&time={}&amount={}', 'removeDailyFeed': 'http://api.petktasia.com/latest/feeder/remove_dailyfeed?deviceId={}&day={}&id=d{}', 'dailyfeeds': 'http://api.petktasia.com/latest/feeder/dailyfeeds?deviceId={}&days={}', 'restoreDailyFeeds': 'http://api.petktasia.com/latest/feeder/restore_dailyfeed?deviceId={}&day={}&id=s{}', 'disableDailyFeeds': 'http://api.petktasia.com/latest/feeder/remove_dailyfeed?deviceId={}&day={}&id=s{}', 'resetDesiccant': 'http://api.petktasia.com/latest/feeder/desiccant_reset?deviceId={}', 'updateSettings': 'http://api.petktasia.com/latest/feeder/update?id={}&kv={}', }, '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={}', } }, 'FeederMini': { 'cn': { 'owndevices': 'http://api.petkit.cn/6/discovery/device_roster', 'deviceState': 'http://api.petkit.cn/6/feedermini/devicestate?id={}', 'deviceDetailInfo': 'http://api.petkit.cn/6/feedermini/device_detail?id={}', 'saveDailyFeed': 'http://api.petkit.cn/6/feedermini/save_dailyfeed?deviceId={}&day={}&time={}&amount={}', 'removeDailyFeed': 'http://api.petkit.cn/6/feedermini/remove_dailyfeed?deviceId={}&day={}&id=d{}', 'dailyfeeds': 'http://api.petkit.cn/6/feedermini/dailyfeeds?deviceId={}&days={}', 'restoreDailyFeeds': 'http://api.petkit.cn/6/feedermini/restore_dailyfeed?deviceId={}&day={}&id=s{}', 'disableDailyFeeds': 'http://api.petkit.cn/6/feedermini/remove_dailyfeed?deviceId={}&day={}&id=s{}', 'resetDesiccant': 'http://api.petkit.cn/6/feedermini/desiccant_reset?deviceId={}', 'updateSettings': 'http://api.petkit.cn/6/feedermini/update?id={}&kv={}', }, 'asia':{ 'owndevices': 'http://api.petktasia.com/latest/discovery/device_roster', 'deviceState': 'http://api.petktasia.com/latest/feedermini/devicestate?id={}', 'deviceDetailInfo': 'http://api.petktasia.com/latest/feedermini/device_detail?id={}', 'saveDailyFeed': 'http://api.petktasia.com/latest/feedermini/save_dailyfeed?deviceId={}&day={}&time={}&amount={}', 'removeDailyFeed': 'http://api.petktasia.com/latest/feedermini/remove_dailyfeed?deviceId={}&day={}&id=d{}', 'dailyfeeds': 'http://api.petktasia.com/latest/feedermini/dailyfeeds?deviceId={}&days={}', 'restoreDailyFeeds': 'http://api.petktasia.com/latest/feedermini/restore_dailyfeed?deviceId={}&day={}&id=s{}', 'disableDailyFeeds': 'http://api.petktasia.com/latest/feedermini/remove_dailyfeed?deviceId={}&day={}&id=s{}', 'resetDesiccant': 'http://api.petktasia.com/latest/feedermini/desiccant_reset?deviceId={}', 'updateSettings': 'http://api.petktasia.com/latest/feedermini/update?id={}&kv={}', }, 'north_america':{ 'owndevices': 'http://api.petkt.com/latest/discovery/device_roster', 'deviceState': 'http://api.petkt.com/latest/feedermini/devicestate?id={}', 'deviceDetailInfo': 'http://api.petkt.com/latest/feedermini/device_detail?id={}', 'saveDailyFeed': 'http://api.petkt.com/latest/feedermini/save_dailyfeed?deviceId={}&day={}&time={}&amount={}', 'removeDailyFeed': 'http://api.petkt.com/latest/feedermini/remove_dailyfeed?deviceId={}&day={}&id=d{}', 'dailyfeeds': 'http://api.petkt.com/latest/feedermini/dailyfeeds?deviceId={}&days={}', 'restoreDailyFeeds': 'http://api.petkt.com/latest/feedermini/restore_dailyfeed?deviceId={}&day={}&id=s{}', 'disableDailyFeeds': 'http://api.petkt.com/latest/feedermini/remove_dailyfeed?deviceId={}&day={}&id=s{}', 'resetDesiccant': 'http://api.petkt.com/latest/feedermini/desiccant_reset?deviceId={}', 'updateSettings': 'http://api.petkt.com/latest/feedermini/update?id={}&kv={}', } } }, 'config': { 'min_amount': 0, // in meal(same in app) 'max_amount': 10, // in meal(same in app) 'min_desiccantLeftDays': 0, // in day 'max_desiccantLeftDays': 30, // in day 'min_batteryLevel': 0, // level(same in app) 'max_batteryLevel': 4, // level(same in app) 'batteryPersentPerLevel': 100 / 4, 'min_pollint_interval': 60, // in second 'max_pollint_interval': 3600, // in second 'min_fetch_status_interval': 10, // in second 'foodStorage_alerm_threshold': 300, } }); class PetkitFeederDevice { constructor() { let accessory = undefined; this.config = undefined; this.events = { 'polling_event': undefined } this.services = { 'drop_meal_service': undefined, 'meal_amount_service': undefined, 'food_storage_service': undefined, 'desiccant_level_service': undefined, 'lightMode_service': undefined, 'battery_status_service': undefined, 'info_service': undefined }; this.status = { // petkit status value 'lastUpdate': 0, 'food' : 0, // this.config.get('reverse_foodStorage_indicator') ? !petkitDevice.status.food : petkitDevice.status.food 'batteryPower': 0, 'batteryStatus': 1, 'desiccantLeftDays' : 0, // petkitDevice.status.desiccantLeftDays < petkitDevice.config.get('alert_desiccant_threshold') ? 1 : 0 'manualLock': 0, 'lightMode': 0, 'meals': {} }; this.savedData = { 'mealAmount': 2, }; Object.defineProperty(this, 'accessory', { get() { return accessory; }, set(value) { accessory = value; this.load(); } }); this.load(); } save() { if (this.accessory && this.savedData) { this.accessory.context = this.savedData; } } load() { if (this.accessory && this.accessory.context) { this.savedData = Object.assign(this.savedData, this.accessory.context); } } getFoodStatusForHomebridge() { if (this.config.get('model') === 'Feeder') { if (this.config.get('reverse_foodStorage_indicator')) { return (this.status.food < globalVariables.config.foodStorage_alerm_threshold ? 1 : 0); } else { return (this.status.food < globalVariables.config.foodStorage_alerm_threshold ? 0 : 1); } } else if (this.config.get('model') === 'FeederMini') { if (this.config.get('reverse_foodStorage_indicator')) { return (this.status.food != 1); } else { return (this.status.food == 1); } } else { return 0; } } }; function getTimestamp() { return Math.floor(Date.now() / 1000); }; function getDataString() { return dayjs(new Date()).format('YYYYMMDD'); } class petkit_feeder_plugin { constructor(log, config, api) { this.log = new logUtil(log, config.log_level || logUtil.LOGLV_INFO); this.log.info('begin to initialize Petkit Feeder Platform.'); if (!api) { this.log.error("Homebridge's version is too old, please upgrade!"); return; } this.api = api; this.accessories = new Map(); if (!config || !config.devices) { this.log.error("no configure found for Petkit Feeder device."); this.log.error("you may need to convert old config to new config? or double check your config.json"); this.log.error("goto https://github.com/elfive/homebridge-petkit-feeder-mini/wiki/How-to-convert-v1.x.x-config-to-v2.x.x for more detail."); return; } else { // When this event is fired, homebridge restored all cached accessories from disk and did call their respective // `configureAccessory` method for all of them. Dynamic Platform plugins should only register new accessories // after this event was fired, in order to ensure they weren't added to homebridge already. // This event can also be used to start discovery of new accessories. this.api.on('didFinishLaunching', () => { // check config usability config.devices.forEach(device_config => { // probably parse config or something here const config = this.configCheck(device_config); if (config) { this.initializeAccessory(config); } }); }); this.log.info('Petkit Feeder Platform loaded.'); } } // check and modify configure value // @prama config: config Object from config.json // @return: if success return instance of configUtil or failed return undefined; configCheck(config) { const fulfill_headerset = (headers, key, value) => { let header = headers.find(header => header.key == key); if (undefined === header) { this.log.debug(format('missing header: {0}, using \'{1}\' instead.', key, value)); headers.push({'key': key, 'value': value}); } else if ('' === header) { this.log.warn(format('header \'{0}\' value is empty, using \'{1}\' instead.', key, value)); headers.push({'key': key, 'value': value}); } }; const conf = new configUtil(config); // required configure items let config_headers = conf.get('headers'); if (!config_headers) { this.log.error('missing dataset: headers in your config.'); return undefined; } let header_x_session = config_headers.find(header => header.key === 'X-Session'); if (undefined === header_x_session) { this.log.error('missing header in your headers: X-Session(note: case sensitive).'); return undefined; } // device model const device_model = conf.fulfill('model', 'FeederMini'); if (-1 === globalVariables.support_device_type.indexOf(device_model)) { this.log.error(format('unsupported device type: {}.', device_model)); return undefined; } // location let location = conf.get('location'); if (!location) { this.log.error('missing dataset: location in your config.'); return undefined; } else { const validLocations = Object.keys(globalVariables.global_urls[device_model]); if (!conf.checkValueValid('location', validLocations)) { this.log.error(format('value of location({0}) should be one of {1}', location, JSON.stringify(validLocations))); return undefined; } } for (const [key, value] of Object.entries(globalVariables.default_headers)) { fulfill_headerset(config_headers, key, value); } // convert header format let http_headers = {}; config_headers.forEach(header => { http_headers[header.key] = header.value; }); conf.set('headers', http_headers) // urls conf.set('urls', globalVariables.global_urls[device_model][location]); // optional configure items // conf.fulfill('name', 'PetkitFeederMini'); // conf.fulfill('sn', 'PetkitFeederMini'); // conf.fulfill('firmware', '0.0.0') conf.fulfill('manufacturer', 'Petkit'); conf.fulfill('enable_polling', true); const polling_interval = conf.get('polling_interval'); const min_polling_interval = globalVariables.config.min_pollint_interval; const max_polling_interval = globalVariables.config.max_pollint_interval; if (polling_interval < min_polling_interval) { this.log.warn(format('value of polling_interval({0}) should great than {1}, now using {1} instead', polling_interval, min_polling_interval)); conf.set('polling_interval', min_polling_interval); } else if (polling_interval > max_polling_interval) { this.log.warn(format('value of polling_interval({0}) should smaller than {1}, now using {1} instead', polling_interval, max_polling_interval)); conf.set('polling_interval', max_polling_interval); } conf.fulfill('enable_manualLock', false); conf.fulfill('enable_lightMode', false); conf.fulfill('reverse_foodStorage_indicator', false); conf.fulfill('fast_response', false); // service name field conf.fulfill('DropMeal_name', 'DropMeal'); conf.fulfill('FoodStorage_name', (conf.get('reverse_foodStorage_indicator') ? 'FoodStorage_Empty' : 'FoodStorage')); conf.fulfill('DesiccantLevel_name', 'DesiccantLevel'); conf.fulfill('ManualLock_name', 'ManualLock'); conf.fulfill('LightMode_name', 'LightMode'); conf.fulfill('Battery_name', 'Battery'); // print config log conf.print(content => {this.log.debug(content)}); return conf; } // REQUIRED - Homebridge will call the "configureAccessory" method once for every cached accessory restored // This function is invoked when homebridge restores cached accessories from disk at startup. // It should be used to setup event handlers for characteristics and update respective values. configureAccessory(accessory) { const petkitDevice = Object.assign(new PetkitFeederDevice(), { 'accessory': accessory, }); this.accessories.set(accessory.UUID, petkitDevice); } // setup accessory's service include handlers,value // @return: if success return true or failed return false; setupAccessory(petkitDevice) { let accessory = petkitDevice.accessory; let config = petkitDevice.config; // instance of configUtil let service_name = undefined; let service_status = undefined; // setup meal drop service if (true) { service_name = config.get('DropMeal_name'); let drop_meal_service = accessory.getService(service_name); if (!drop_meal_service) { // service not exist, create service drop_meal_service = accessory.addService(Service.Switch, service_name, service_name); if (!drop_meal_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } drop_meal_service.getCharacteristic(Characteristic.On) .on('get', callback => callback(null, 0)) .on('set', this.hb_dropMeal_set.bind(this, petkitDevice)); petkitDevice.services.drop_meal_service = drop_meal_service; } // setup meal amount service if (true) { service_name = config.get('MealAmount_name'); let meal_amount_service = accessory.getService(service_name); if (!meal_amount_service) { // service not exist, create service meal_amount_service = accessory.addService(Service.Fan, service_name, service_name); if (!meal_amount_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } meal_amount_service.getCharacteristic(Characteristic.On) .on('get', callback => callback(null, petkitDevice.savedData.mealAmount !== 0)); meal_amount_service.getCharacteristic(Characteristic.RotationSpeed) .on('get', callback => callback(null, petkitDevice.savedData.mealAmount)) .on('set', this.hb_mealAmount_set.bind(this, petkitDevice)) .setProps({ minValue: globalVariables.config.min_amount, maxValue: globalVariables.config.max_amount, minStep: 1 }); petkitDevice.services.meal_amount_service = meal_amount_service; } // setup food storage indicator service if (true) { service_name = config.get('FoodStorage_name'); let food_storage_service = accessory.getService(service_name); if (!food_storage_service) { // service not exist, create service food_storage_service = accessory.addService(Service.OccupancySensor, service_name, service_name); if (!food_storage_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } service_status = petkitDevice.getFoodStatusForHomebridge(); food_storage_service.setCharacteristic(Characteristic.OccupancyDetected, service_status) food_storage_service.getCharacteristic(Characteristic.OccupancyDetected) .on('get', this.hb_foodStorageStatus_get.bind(this, petkitDevice)); petkitDevice.services.food_storage_service = food_storage_service; } // setup desiccant left days service if (config.get('enable_desiccant')) { service_name = config.get('DesiccantLevel_name'); let desiccant_level_service = accessory.getService(service_name); if (!desiccant_level_service) { // service not exist, create service desiccant_level_service = accessory.addService(Service.FilterMaintenance, service_name, service_name); if (!desiccant_level_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } desiccant_level_service.setCharacteristic(Characteristic.FilterChangeIndication, (petkitDevice.status.desiccantLeftDays < this.alert_desiccant_threshold ? 1 : 0)); desiccant_level_service.getCharacteristic(Characteristic.FilterChangeIndication) .on('get', this.hb_desiccantIndicator_get.bind(this, petkitDevice)); desiccant_level_service.setCharacteristic(Characteristic.FilterLifeLevel, petkitDevice.status.desiccantLeftDays); desiccant_level_service.getCharacteristic(Characteristic.FilterLifeLevel) .on('get', this.hb_desiccantLeftDays_get.bind(this, petkitDevice)) .setProps({ minValue: globalVariables.config.min_desiccantLeftDays, maxValue: globalVariables.config.max_desiccantLeftDays, minStep: 1 }); desiccant_level_service.getCharacteristic(Characteristic.ResetFilterIndication) .on('set', this.hb_desiccantLeftDays_reset.bind(this, petkitDevice)); petkitDevice.services.desiccant_level_service = desiccant_level_service; } // setup manualLock setting service if (config.get('enable_manualLock')) { service_name = config.get('ManualLock_name'); let manualLock_service = accessory.getService(service_name); if (!manualLock_service) { // service not exist, create service manualLock_service = accessory.addService(Service.Switch, service_name, service_name); if (!manualLock_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } manualLock_service.setCharacteristic(Characteristic.On, petkitDevice.status.manualLock); manualLock_service.getCharacteristic(Characteristic.On) .on('get', this.hb_manualLockStatus_get.bind(this, petkitDevice)) .on('set', this.hb_manualLockStatus_set.bind(this, petkitDevice)); petkitDevice.services.manualLock_service = manualLock_service; } // setup lightMode setting service if (config.get('enable_lightMode')) { service_name = config.get('LightMode_name'); let lightMode_service = accessory.getService(service_name); if (!lightMode_service) { // service not exist, create service lightMode_service = accessory.addService(Service.Switch, service_name, service_name); if (!lightMode_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } lightMode_service.setCharacteristic(Characteristic.On, petkitDevice.status.lightMode); lightMode_service.getCharacteristic(Characteristic.On) .on('get', this.hb_lightModeStatus_get.bind(this, petkitDevice)) .on('set', this.hb_lightModeStatus_set.bind(this, petkitDevice)); petkitDevice.services.lightMode_service = lightMode_service; } // setup battery status service, only for Petkit Feeder Mini if (petkitDevice.config.get('model') === 'FeederMini') { service_name = config.get('Battery_name'); let battery_status_service = accessory.getService(service_name); if (!battery_status_service) { // service not exist, create service battery_status_service = accessory.addService(Service.BatteryService, service_name, service_name); if (!battery_status_service) { this.log.error('petkit device service create failed: ' + service_name); return false; } } battery_status_service.setCharacteristic(Characteristic.BatteryLevel, petkitDevice.status.batteryPower); battery_status_service.getCharacteristic(Characteristic.BatteryLevel) .on('get', this.hb_deviceBatteryLevel_get.bind(this, petkitDevice)); battery_status_service.setCharacteristic(Characteristic.ChargingState, (petkitDevice.status.batteryStatus == 0 ? Characteristic.ChargingState.CHARGING : Characteristic.ChargingState.NOT_CHARGING)); battery_status_service.getCharacteristic(Characteristic.ChargingState) .on('get', this.hb_deviceChargingState_get.bind(this, petkitDevice)); battery_status_service.setCharacteristic(Characteristic.StatusLowBattery, (petkitDevice.status.batteryPower <= 50 ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL)); battery_status_service.getCharacteristic(Characteristic.StatusLowBattery) .on('get', this.hb_deviceStatusLowBattery_get.bind(this, petkitDevice)); petkitDevice.services.battery_status_service = battery_status_service; } // setup divice information service if (true) { let info_service = accessory.getService(Service.AccessoryInformation); if (!info_service) { // service not exist, create service info_service = accessory.addService(Service.AccessoryInformation); if (!info_service) { this.log.error('petkit device service create failed: info_service'); return false; } } info_service .setCharacteristic(Characteristic.Identify, config.get('deviceId')) .setCharacteristic(Characteristic.Manufacturer, config.get('manufacturer')) .setCharacteristic(Characteristic.Model, config.get('model')) .setCharacteristic(Characteristic.SerialNumber, config.get('sn')) // infomation below changed from petkit app require a homebridge reboot to take effect. .setCharacteristic(Characteristic.Name, config.get('name')) .setCharacteristic(Characteristic.FirmwareRevision, config.get('firmware')); petkitDevice.services.info_service = info_service; } return true; } // initialize one accessory // @param config: instance of configUtil initializeAccessory(config) { this.log.info('initializing Petkit Feeder device.'); // get deviceId from server let validDevice = undefined; this.log.debug('request device info from Petkit server.'); this.http_getOwnDevice(config) .then(owned_device_raw => { if (owned_device_raw) { const user_deviceId = config.get('deviceId'); const owned_devices = this.praseGetOwnedDevice(owned_device_raw); if (owned_devices.length === 0) { this.log.error(format('sorry that this plugin only works with these device type:{}.', JSON.stringify(globalVariables.support_device_type))); } else if (owned_devices.length === 1) { if (!user_deviceId || owned_devices[0].id == user_deviceId) { this.log.info(format('found you ownd one {} with deviceId: {}.', owned_devices[0].type, owned_devices[0].id)); } else { this.log.warn(format('found you just ownd one {} with deviceId: ', owned_devices[0].type , owned_devices[0].id)); this.log.warn(format('which is not the same with the deviceId you set: {}', user_deviceId)); this.log.warn(format('will use {} instead', owned_devices[0].id)); } validDevice = owned_devices[0]; } else { let match_device = owned_devices.find(device => user_deviceId && device.id == user_deviceId); if (undefined === match_device) { const devicesIds = owned_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(format('do you mean one of this: ', JSON.stringify(devicesIds))); } else { this.log.info(format('found you ownd one {} with deviceId: {}', match_device.type, match_device.id)); validDevice = match_device; } } } else { this.log.error('unable to fetch information from petkit server. skip adding this petkit device.'); } }) .catch(error => { this.log.error('unable to determine whether the deviceId you set is valid: ' + error.stack ? error.stack : error); }) .then(() => { if (!validDevice) { this.log.error('initialize Petkit Feeder failed: could not find supported device.'); return; } config.set('deviceId', validDevice.id); config.set('name', validDevice.name); config.set('model', validDevice.type); // uuid must be generated from a unique but not changing data source, // name should not be used in the most cases. But works in this specific example. const uuid = UUIDGen.generate(validDevice.name); let petkitDevice = this.accessories.get(uuid); if (!petkitDevice) { // petkitDevice not exists, create petkitDevice and accessory let accessory = new this.api.platformAccessory(validDevice.name, uuid, validDevice.name); if (!accessory) { this.log.error('initialize Petkit Feeder failed: could not create accessory'); return; } petkitDevice = new PetkitFeederDevice(); petkitDevice.accessory = accessory; petkitDevice.config = config; } else if (!petkitDevice.accessory) { // accessory not exists, create accessory let accessory = new this.api.platformAccessory(validDevice.name, uuid, validDevice.name); if (!accessory) { this.log.error('initialize Petkit Feeder failed: could not create accessory'); return; } petkitDevice.accessory = accessory; petkitDevice.config = config; } else { // accessory exists petkitDevice.config = config; } this.log.debug('request initial device status from Petkit server.'); this.http_getDeviceDetailStatus(petkitDevice, deviceDetailInfo => { if (deviceDetailInfo) { petkitDevice.config.set('sn', deviceDetailInfo.sn); petkitDevice.config.set('firmware', deviceDetailInfo.firmware); petkitDevice.config.assign('headers', {key: 'X-TimezoneId', value: deviceDetailInfo.locale}) if (this.setupAccessory(petkitDevice)) { // all service setup success, now update accessory if (!this.accessories.get(uuid)) { this.api.registerPlatformAccessories(pluginName, platformName, [petkitDevice.accessory]); } this.accessories.set(uuid, petkitDevice.accessory); this.log.info(format('initialize Petkit Feeder device({}) success.', config.get('name'))); // polling this.setupPolling(petkitDevice); } else { this.log.error(format('initialize Petkit Feeder device({}) failed.', config.get('name'))); } } else { this.log.warn(format('bypass initialize Petkit Feeder device({}).', config.get('name'))); } }); }); } praseGetOwnedDevice(jsonObj) { if (!jsonObj) { this.log.error('praseGetOwnedDevice 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.hasOwnProperty('devices')) { this.log.error('JSON.parse error with:' + jsonStr); return false; } if (jsonObj.result.devices.length === 0) { this.log.error('seems you didn\'t owned a Petkit Feeder device.'); return false; } var valid_devices = []; jsonObj.result.devices.forEach(device => { const index = globalVariables.support_device_type.indexOf(device.type); if (index !== -1 && device.data) { valid_devices.push(Object.assign(device.data, {'type': device.type})); } }); return valid_devices; } praseGetDeviceDetailInfo(jsonObj) { if (!jsonObj) { 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 (!jsonObj.hasOwnProperty('result') || !jsonObj.result.hasOwnProperty('state') || !jsonObj.result.hasOwnProperty('settings')) { this.log.error('unable to parse device info reply from server with data: ' + jsonStr); return false; } const deviceInfo = jsonObj.result; let deviceDetailInfo = {}; if (deviceInfo.name) deviceDetailInfo.name = deviceInfo.name; if (deviceInfo.sn) deviceDetailInfo.sn = deviceInfo.sn; if (deviceInfo.firmware) deviceDetailInfo.firmware = deviceInfo.firmware; if (deviceInfo.timezone) deviceDetailInfo.timezone = deviceInfo.timezone; if (deviceInfo.locale) deviceDetailInfo.locale = deviceInfo.locale; deviceDetailInfo.status = {}; // 1 for status ok, 0 for empty if (deviceInfo.state.food !== undefined) deviceDetailInfo.status.food = deviceInfo.state.food; // this.log.debug('device food storage status is: ' + (deviceDetailInfo.status.food ? 'Ok' : 'Empty')); if (deviceInfo.state.batteryPower !== undefined) deviceDetailInfo.status.batteryPower = deviceInfo.state.batteryPower; // this.log.debug('device battery level is: ' + deviceDetailInfo.status.batteryPower * globalVariables.config.batteryPersentPerLevel); // 0 for charging mode, 1 for battery mode if (deviceInfo.state.batteryStatus !== undefined) deviceDetailInfo.status.batteryStatus = deviceInfo.state.batteryStatus; // this.log.debug('device battery status is: ' + (deviceDetailInfo.status.batteryStatus ? 'charging mode' : 'battery mode')); if (deviceInfo.state.desiccantLeftDays !== undefined) deviceDetailInfo.status.desiccantLeftDays = deviceInfo.state.desiccantLeftDays; // this.log.debug('device desiccant remain: ' + (deviceDetailInfo.status.desiccantLeftDays + ' day(s)')); // 0 for unlocked, 1 for locked if (deviceInfo.settings.manualLock !== undefined) deviceDetailInfo.status.manualLock = deviceInfo.settings.manualLock; // this.log.debug('device manual lock status is: ' + (deviceDetailInfo.status.manualLock ? 'unlocked' : 'locked')); // 0 for light off, 1 for lignt on if (deviceInfo.settings.lightMode !== undefined) deviceDetailInfo.status.lightMode = deviceInfo.settings.lightMode; // this.log.debug('device light status is: ' + (deviceDetailInfo.status.lightMode ? 'on' : 'off')); return deviceDetailInfo; } 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; } return (jsonObj.result.isExecuted === 1); } // success return data, failed return undefined async http_request(options) { // return.data and return.error are mutual exclusion const request_once = async options => { return new Promise(resolve => { let result = undefined; axios.request(options) .then(response => { if (response.status != 200) { result = {error: 'http request received a invalid response code: ' + response.status}; } else { this.log.debug('http request success') result = {data: response.data}; } }) .catch(error => { result = {error: 'http request failed: ' + error}; }) .then(() => { resolve(result); }); }); }; let result = undefined; result = await request_once(options); // retry logic if (result.error && options.retry.enabled && options.timeout > 0) { const max_retry = globalVariables.default_http_options.retry.max_retry; for (let retry = 2; retry <= max_retry; retry++) { result = await request_once(options); if (result.error) { this.log.warn(result.error); this.log.warn(format('retry http request: {}/{}', retry, max_retry)); } else if (result.data) { break; } } } if (result.error) { this.log.error(result.error); this.log.error(format('http request failed: ' + result.error)); } return result.data; } async http_getOwnDevice(config) { const url = config.get('urls').owndevices; const options = Object.assign(globalVariables.default_http_options, { url: url, headers: config.get('headers'), responseType: 'json' }); return await this.http_request(options); } async http_getDeviceInfo(petkitDevice) { const deviceId = petkitDevice.config.get('deviceId'); const url_template = petkitDevice.config.get('urls').deviceDetailInfo; const url = format(url_template, deviceId); const options = Object.assign(globalVariables.default_http_options, { url: url, headers: petkitDevice.config.get('headers'), responseType: 'json' }); return await this.http_request(options); } async http_getDeviceDailyFeeds(petkitDevice) { const date = getDataString(); const deviceId = petkitDevice.config.get('deviceId'); const url_template = petkitDevice.config.get('urls').dailyfeeds; const url = format(url_template, deviceId, date); const options = Object.assign(globalVariables.default_http_options, { url: url, headers: petkitDevice.config.get('headers'), responseType: 'json' }); return await this.http_request(options); } async http_getDeviceState(petkitDevice) { // { // "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" // } // } // } const deviceId = petkitDevice.config.get('deviceId'); const url_template = petkitDevice.config.get('urls').deviceState; const url = format(url_template, deviceId); const options = Object.assign(globalVariables.default_http_options, { url: url, headers: petkitDevice.config.get('headers'), responseType: 'json' }); return await this.http_request(options); } // date:20200920、time: 68400(-1 stand for current)、amount in app unit,1 for 5g, 10 is max(50g) async http_saveDailyFeed(petkitDevice, amount, time) { const date = getDataString(); const deviceId = petkitDevice.config.get('deviceId'); const url_template = petkitDevice.config.get('urls').saveDailyFeed; const url = format(url_template, deviceId, date, time, amount * 5); const options = Object.assign(globalVariables.default_http_options, { url: url, headers: petkitDevice.config.get('headers'), responseType: 'json' }); return await this.http_request(options); } // key see support_settings. async http_updateDeviceSettings(petkitDevice, key, value) { const setting_key = globalVariables.support_settings[key]; if (setting_key !== undefined) { let data = {}; data[setting_key] = value; const deviceId = petkitDevice.config.get('deviceId'); const url_template = petkitDevice.config.get('urls').updateSettings; const url = format(url_template, deviceId, JSON.stringify(data)); const options = Object.assign(globalVariables.default_http_options, { url: url, headers: petkitDevice.config.get('headers'), responseType: 'json' }); return await this.http_request(options); } else { this.log.warn('unsupport setting: ' + key); return false; } } async http_resetDesiccant(petkitDevice) { const deviceId = petkitDevice.config.get('deviceId'); const url_template = petkitDevice.config.get('urls').resetDesiccant; const url = format(url_template, deviceId); const options = Object.assign(globalVariables.default_http_options, { url: url, headers: petkitDevice.config.get('headers'), responseType: 'json' }); return await this.http_request(options); } async http_getDeviceDetailStatus(petkitDevice, callback) { let deviceDetailInfo = undefined; this.http_getDeviceInfo(petkitDevice) .then(device_detail_raw => { deviceDetailInfo = this.praseGetDeviceDetailInfo(device_detail_raw); }) .catch(error => { this.log.error(format('unable to get device({}) status: {}', petkitDevice.config.get('deviceId'), error)); }) .then(() => { if (deviceDetailInfo) { petkitDevice.status = Object.assign(petkitDevice.status, deviceDetailInfo.status); petkitDevice.status.lastUpdate = getTimestamp(); } if (callback) callback(deviceDetailInfo); }); } uploadStatusToHomebridge(petkitDevice) { let service = undefined; let service_status = undefined; this.log.debug(JSON.stringify(petkitDevice.status)); // battery service only for Petkit Feeder Mini if (petkitDevice.config.get('model') === 'FeederMini') { // battery service = petkitDevice.services.battery_status_service; // battery level service_status = petkitDevice.status.batteryPower * globalVariables.config.batteryPersentPerLevel; this.log.info(format('battery level is {}%.', service_status)); service.setCharacteristic(Characteristic.BatteryLevel, service_status); // charging state if (petkitDevice.status.batteryStatus === 0) { service_status = Characteristic.ChargingState.CHARGING; this.log.info('battery is charging.'); } else { service_status = Characteristic.ChargingState.NOT_CHARGING; this.log.info('battery is not charging.'); } service.setCharacteristic(Characteristic.ChargingState, service_status); // low battery status if (petkitDevice.status.batteryStatus !== 0 && !petkitDevice.config.get('ignore_battery_when_charging') && petkitDevice.status.batteryPower * globalVariables.config.batteryPersentPerLevel <= 50) { service_status = Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; this.log.info('battery level status is low'); } else { service_status = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; this.log.info('battery level status is normal'); } service.setCharacteristic(Characteristic.StatusLowBattery, service_status); } // manualLock if (petkitDevice.config.get('enable_manualLock')) { service = petkitDevice.services.manualLock_service; service_status = petkitDevice.status.manualLock === 0; this.log.info(format('manualLock status is {}.', service_status ? 'on' : 'off'));