Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xiaomi Human Body Movement Sensor #511

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/constants/DevTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ ALL_DEVICES.PLANT_MONITOR = {
'specTypes': ['Plant Monitor', 'plant-monitor']
}

ALL_DEVICES.MOTION_SENSOR = {
'type': 'MotionSensor',
'specTypes': ['Motion Sensor', 'motion-sensor']
}

const identifyDeviceBySpecType = (specType) => {
if (specType) {
let devTypeKey = Object.keys(ALL_DEVICES).find( tmpDevTypeKey => ALL_DEVICES[tmpDevTypeKey].specTypes.map(item => item.toLowerCase()).includes(specType.toLowerCase()));
Expand Down Expand Up @@ -214,4 +219,5 @@ module.exports.PET_FEEDER = ALL_DEVICES.PET_FEEDER.type;
module.exports.TEMPERATURE_HUMIDITY_SENSOR = ALL_DEVICES.TEMPERATURE_HUMIDITY_SENSOR.type;
module.exports.SUBMERSION_SENSOR = ALL_DEVICES.SUBMERSION_SENSOR.type;
module.exports.PLANT_MONITOR = ALL_DEVICES.PLANT_MONITOR.type;
module.exports.MOTION_SENSOR = ALL_DEVICES.MOTION_SENSOR.type;
module.exports.identifyDeviceBySpecType = identifyDeviceBySpecType;
1 change: 1 addition & 0 deletions lib/constants/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
MIOT_DEVICE_INITIAL_PROPERTY_FETCH_DONE: 'miotDeviceInitialPropertyFetchDone',
MIOT_DEVICE_PROPERTY_VALUE_UPDATED: 'miotDevicePropertyValueUpdated',
MIOT_DEVICE_PROPERTY_VALUE_SET: 'miotDevicePropertyValueSet',
MIOT_DEVICE_PROPERTY_RETRIEVED: 'miotDevicePropertyRetrieved',
MIOT_DEVICE_ACTION_EXECUTED: 'miotDeviceActionExecuted',
MIOT_DEVICE_METHOD_EXECUTED: 'miotDeviceMethodExecuted',
MIOT_DEVICE_ALL_PROPERTIES_UPDATED: 'miotDeviceAllPropertiesUpdated',
Expand Down
110 changes: 110 additions & 0 deletions lib/modules/motionsensor/MotionSensorAccessory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
let Service, Characteristic, Accessory, HapStatusError, HAPStatus;
const BaseAccessory = require('../../base/BaseAccessory.js');
const Constants = require('../../constants/Constants.js');
const DevTypes = require('../../constants/DevTypes.js');


class MotionSensorAccessory extends BaseAccessory {
constructor(name, device, uuid, config, api, logger) {

Service = api.hap.Service;
Characteristic = api.hap.Characteristic;
Accessory = api.platformAccessory;
HapStatusError = api.hap.HapStatusError;
HAPStatus = api.hap.HAPStatus;

super(name, device, uuid, config, api, logger);
}


/*----------========== INIT ==========----------*/

initAccessoryObject() {
super.initAccessoryObject();
}


/*----------========== ACCESSORY INFO ==========----------*/

getAccessoryType() {
return DevTypes.MOTION_SENSOR;
}


/*----------========== INIT ACCESSORIES ==========----------*/

initAccessories(name, uuid) {
return [new Accessory(name, uuid, this.api.hap.Accessory.Categories.SENSOR)];
}


/*----------========== SETUP SERVICES ==========----------*/

setupMainAccessoryService() {
this.motionSensorService = new Service.MotionSensor(this.getName(), 'motionSensorService');
this.motionSensorService
.getCharacteristic(Characteristic.MotionDetected)
.onGet(this.getMotionDetectedState.bind(this));
this.motionSensorService
.addCharacteristic(Characteristic.StatusActive)
.onGet(this.getMotionSensorStatusActive.bind(this));

this.addAccessoryService(this.motionSensorService);
}

setupAdditionalAccessoryServices() {
super.setupAdditionalAccessoryServices(); // make sure we call super
}


/*----------========== CREATE ADDITIONAL SERVICES ==========----------*/


/*----------========== HOMEBRIDGE STATE SETTERS/GETTERS ==========----------*/

getMotionDetectedState() {
if (this.isMiotDeviceConnected()) {
return this.getDevice().isMotionStateOn();
}
return false;
}

getMotionSensorStatusActive() {
return this.isMiotDeviceConnected();
}


// ----- additional services


/*----------========== STATUS ==========----------*/

updateAccessoryStatus() {
if (this.motionSensorService) {
this.motionSensorService.getCharacteristic(Characteristic.MotionDetected).updateValue(this.getMotionDetectedState());
this.motionSensorService.getCharacteristic(Characteristic.StatusActive).updateValue(this.getMotionSensorStatusActive());
}

super.updateAccessoryStatus();
}


/*----------========== MULTI-SWITCH SERVICE HELPERS ==========----------*/


/*----------========== GETTERS ==========----------*/


/*----------========== PROPERTY WRAPPERS ==========----------*/


/*----------========== PROPERTY HELPERS ==========----------*/


/*----------========== HELPERS ==========----------*/


}


module.exports = MotionSensorAccessory;
138 changes: 138 additions & 0 deletions lib/modules/motionsensor/MotionSensorDevice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const BaseDevice = require('../../base/BaseDevice.js');
const Constants = require('../../constants/Constants.js');
const DevTypes = require('../../constants/DevTypes.js');
const PropFormat = require('../../constants/PropFormat.js');
const PropUnit = require('../../constants/PropUnit.js');
const PropAccess = require('../../constants/PropAccess.js');
const Events = require('../../constants/Events.js');


class MotionSensorDevice extends BaseDevice {
constructor(device, name, logger) {
super(device, name, logger);

// Only needed if device need to process motion status based on response updateTime
this.propResponse = null;
this.lastResponse = null;
this.lastMotionResponse = null;
}


/*----------========== LIFECYCLE ==========----------*/

initialPropertyFetchDone() {
super.initialPropertyFetchDone();
}


/*----------========== DEVICE INFO ==========----------*/

getType() {
return DevTypes.MOTION_SENSOR;
}

getDeviceName() {
return 'Unknown motion sensor device';
}

getMainService() {
return this.getServiceByType('motion-sensor');
}


/*----------========== CONFIG ==========----------*/

propertiesToMonitor() {
return ['motion-sensor:motion-state', 'battery:battery-level'];
}


/*----------========== VALUES ==========----------*/


/*----------========== PROPERTIES ==========----------*/

//overrides


//device specific
motionStateProp() {
return this.getProperty('motion-sensor:motion-state');
}


/*----------========== ACTIONS ==========----------*/


/*----------========== FEATURES ==========----------*/


/*----------========== GETTERS ==========----------*/

isMotionStateOn() {
return this.getPropertyValue(this.motionStateProp());
}


/*----------========== SETTERS ==========----------*/


/*----------========== CONVENIENCE ==========----------*/


/*----------========== VALUE CONVENIENCE ==========----------*/


/*----------========== HELPERS ==========----------*/
_registerForPropRetrieved(miotProp) {
if (miotProp) {
miotProp.on(Events.MIOT_DEVICE_PROPERTY_RETRIEVED, (response) => {
this.propResponse = response;
this.logger.debug(`Property ${miotProp.getName()} response retrieved.`);
});
}
}

_updateValueBasedOnUpdateTimeFromDevice(response) {
let currentTime = Math.floor(Date.now() / 1000); // convert to seconds
let updateTime = response.updateTime;
let motionTimeout = 60; // 60 seconds timeout

if (this.lastResponse && this.lastResponse.updateTime !== updateTime) {
if (currentTime - updateTime <= motionTimeout) {
let lastMotionUpdateTime = this.lastMotionResponse.updateTime;
if (!this._isPeriodicCheck(lastMotionUpdateTime, updateTime)){
response.value = true;
this.lastMotionResponse = {...response};
}
else {
response.value = false;
}
} else {
response.value = false;
}
} else {
// init status
if (!this.lastResponse){
response.value = false;
this.lastMotionResponse = {...response};
}
// no change in updateTime, then we keep the previous status
else{
response.value = this.lastResponse.value;
}
}
this.lastResponse = {...response};
}


_isPeriodicCheck(baseTime, targetTime) {
let difference = Math.abs(targetTime - baseTime);
let remainder = difference % 60;

return remainder === 0;
}

}

module.exports = MotionSensorDevice;
74 changes: 74 additions & 0 deletions lib/modules/motionsensor/devices/lumi.sensor_motion.v2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const MotionSensorDevice = require('../MotionSensorDevice.js');
const Constants = require('../../../constants/Constants.js');
const PropFormat = require('../../../constants/PropFormat.js');
const PropUnit = require('../../../constants/PropUnit.js');
const PropAccess = require('../../../constants/PropAccess.js');
const Events = require('../../../constants/Events.js');


class LumiSensor_motionV2 extends MotionSensorDevice {
constructor(miotDevice, name, logger) {
super(miotDevice, name, logger);
}


/*----------========== DEVICE INFO ==========----------*/

getDeviceName() {
return 'Xiaomi Human Body Movement Sensor';
}

getMiotSpecUrl() {
return 'https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:motion-sensor:0000A014:lumi-v2:2';
}


/*----------========== CONFIG ==========----------*/

requiresMiCloud() {
return true;
}


/*----------========== METADATA ==========----------*/

initDeviceServices() {
this.createServiceByString('{"siid":2,"type":"urn:miot-spec-v2:service:motion-sensor:00007825:lumi-v2:1","description":"Motion Sensor"}');
}

initDeviceProperties() {
const newProp = this.addPropertyByString('motion-sensor:motion-state', '{"siid":2,"piid":1,"type":"urn:miot-spec-v2:property:motion-state:0000007D:lumi-v2:1","description":"Motion State","format":"bool","access":["read","notify"]}');
this._registerForPropRetrieved(newProp);
}

initDeviceActions() {
//no actions
}

initDeviceEvents() {
this.addEventByString('motion-sensor:motion-detected', '{"siid":2,"eiid":1,"type":"urn:miot-spec-v2:event:motion-detected:00005001:lumi-v2:2","description":"Motion Detected","arguments":[]}');
}


/*----------========== VALUES OVERRIDES ==========----------*/


/*----------========== PROPERTY OVERRIDES ==========----------*/
motionStateProp() {
let prop = this.getProperty('motion-sensor:motion-state');
if (this.propResponse && prop){
this._updateValueBasedOnUpdateTimeFromDevice(this.propResponse);
prop.value = this.propResponse.value;
}
return prop;
}

/*----------========== ACTION OVERRIDES ==========----------*/


/*----------========== OVERRIDES ==========----------*/


}

module.exports = LumiSensor_motionV2;
8 changes: 8 additions & 0 deletions lib/protocol/MiotDevice.js
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,7 @@ class MiotDevice extends EventEmitter {
// all good, process the data
const obj = {};
for (let i = 0; i < result.length; i++) {
this._emitPropertyRetrievedFromDevice(propKeys[i], result[i]);
this._updatePropertyValueFromDevice(obj, propKeys[i], result[i]);
}
return obj;
Expand Down Expand Up @@ -1354,6 +1355,13 @@ class MiotDevice extends EventEmitter {
return false;
}

_emitPropertyRetrievedFromDevice(propName, response) {
if (this._isResponseValid(response)) {
this.getPropertyByName(propName).emit(Events.MIOT_DEVICE_PROPERTY_RETRIEVED, response);
} else {
this.logger.debug(`Error while parsing response from device for property ${propName}. Response: ${JSON.stringify(response)}`);
}
}

/*----------========== HELPERS ==========----------*/

Expand Down