Skip to content

Commit

Permalink
* Finished implementation for feature request: #1
Browse files Browse the repository at this point in the history
* Little progress for feature request: #5
* Implemented set Guard Mode with CMD_SET_PAYLOAD for certain devices
* Added back USA ip addresses for P2P cloud discovery
* Using the correct local time zone for communication with the Eufy Cloud
* HUB filtering by device type 0 (station) removed
* Added documentation for 2FA
* Updated versions of the package dependencies
  • Loading branch information
Patrick Broetto committed Dec 22, 2020
1 parent 37e9da1 commit c79071e
Show file tree
Hide file tree
Showing 33 changed files with 609 additions and 296 deletions.
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ One Adapter instance will show all devices from one Eufy Cloud account and allow
* ...
* Actions:
* Change guard mode
* Events:
* Alarm mode change
* Camera:
* States:
* Online / offline etc.
Expand Down Expand Up @@ -72,10 +74,12 @@ One Adapter instance will show all devices from one Eufy Cloud account and allow
* Online / offline etc.
* Low battery
* Two factor authentication (token renewal needs manual intervention)
* Basic P2P communication functionality:
* event: Alarm mode change
* more to come...

## Configuration

See [here](./docs/en/README.md)

## Known working devices

* HomeBase 2 (T8010)
Expand All @@ -89,10 +93,20 @@ If more devices work (or also not) please report them by opening a GitHub issue.

Please use GitHub issues for this.

Best is to set the adapter to Debug log mode (Instances -> Expert mode -> Column Log level). Then please get the logfile from disk (subdirectory "log" in ioBroker installation directory and not from Admin because Admin cuts the lines).
Best is to set the adapter to Debug log mode (Instances -> Expert mode -> Column Log level or see [here](https://github.com/bropat/ioBroker.eufy-security/wiki/Howto-enable-debug)). Then please get the logfile from disk (subdirectory "log" in ioBroker installation directory and not from Admin because Admin cuts the lines).

## Changelog

### 0.0.9 (2020-12-xx)
* (bropat) Finished implementation for feature request: [#1](https://github.com/bropat/ioBroker.eufy-security/issues/1)
* (bropat) Little progress for feature request: [#5](https://github.com/bropat/ioBroker.eufy-security/issues/5)
* (bropat) Implemented set Guard Mode with CMD_SET_PAYLOAD for certain devices
* (bropat) Added back USA ip addresses for P2P cloud discovery
* (bropat) Using the correct local time zone for communication with the Eufy Cloud
* (bropat) HUB filtering by device type 0 (station) removed
* (bropat) Added documentation for 2FA
* (bropat) Updated versions of the package dependencies

### 0.0.8 (2020-12-13)
* (bropat) Fixed issue [#16](https://github.com/bropat/ioBroker.eufy-security/issues/16)
* (bropat) P2P communication revisited
Expand Down
14 changes: 13 additions & 1 deletion build/lib/eufy-security/eufy-security.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,19 @@ class EufySecurity extends events_1.EventEmitter {
this.log.debug(`EufySecurity.stationParameterChanged(): station: ${station.getSerial()} type: ${type} value: ${value}`);
if (type == types_1.ParamType.GUARD_MODE)
//TODO: if configured guard mode was changed to SCHEDULE (2) we get the correct current mode, but we change the effective guard mode on next http data refresh... Get it asap!
utils_1.setStateChangedAsync(this.adapter, station.getStateID(types_1.StationStateID.GUARD_MODE), value);
try {
utils_1.setStateChangedAsync(this.adapter, station.getStateID(types_1.StationStateID.GUARD_MODE), Number.parseInt(value));
}
catch (error) {
this.log.error(`EufySecurity.stationParameterChanged(): station: ${station.getSerial()} GUARD_MODE Error: ${error}`);
}
else if (type == types_1.ParamType.SCHEDULE_MODE)
try {
utils_1.setStateChangedAsync(this.adapter, station.getStateID(types_1.StationStateID.CURRENT_MODE), Number.parseInt(value));
}
catch (error) {
this.log.error(`EufySecurity.stationParameterChanged(): station: ${station.getSerial()} CURRENT_MODE Error: ${error}`);
}
}
deviceParameterChanged(device, type, value) {
this.log.debug(`EufySecurity.deviceParameterChanged(): device: ${device.getSerial()} type: ${type} value: ${value}`);
Expand Down
69 changes: 56 additions & 13 deletions build/lib/eufy-security/http/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ exports.API = void 0;
const axios_1 = __importDefault(require("axios"));
const events_1 = require("events");
const types_1 = require("./types");
const parameter_1 = require("./parameter");
const utils_1 = require("./utils");
class API extends events_1.EventEmitter {
constructor(username, password, log) {
super();
Expand All @@ -24,10 +26,11 @@ class API extends events_1.EventEmitter {
this.password = null;
this.token = null;
this.token_expiration = null;
this.trusted_token_expiration = new Date(2100, 12, 31, 23, 59, 59, 0);
this.devices = {};
this.hubs = {};
this.headers = {
app_version: "v2.2.2_741",
app_version: "v2.3.0_792",
os_type: "android",
os_version: "29",
phone_model: "ONEPLUS A3003",
Expand All @@ -46,6 +49,7 @@ class API extends events_1.EventEmitter {
this.username = username;
this.password = password;
this.log = log;
this.headers.timezone = utils_1.getTimezoneGMTString();
}
invalidateToken() {
this.token = null;
Expand All @@ -55,7 +59,6 @@ class API extends events_1.EventEmitter {
authenticate() {
return __awaiter(this, void 0, void 0, function* () {
//Authenticate and get an access token
//TODO: Finish token renew implementation with 2FA!
this.log.debug(`API.authenticate(): token: ${this.token} token_expiration: ${this.token_expiration}`);
if (!this.token || this.token_expiration && (new Date()).getTime() >= this.token_expiration.getTime()) {
try {
Expand Down Expand Up @@ -158,8 +161,8 @@ class API extends events_1.EventEmitter {
if (response.status == 200) {
const result = response.data;
if (result.code == types_1.ResponseErrorCode.CODE_WHATEVER_ERROR) {
if (response.data && response.data.list) {
return response.data.list;
if (result.data && result.data.list) {
return result.data.list;
}
}
else {
Expand Down Expand Up @@ -199,10 +202,12 @@ class API extends events_1.EventEmitter {
const result = response2.data;
if (result.code == types_1.ResponseErrorCode.CODE_WHATEVER_ERROR) {
this.log.info(`2FA authentication successfully done. Device trusted.`);
// For logging purposes
yield this.listTrustDevice().catch(error => {
this.log.error(`API.listTrustDevice(): error: ${JSON.stringify(error)}`);
return error;
const trusted_devices = yield this.listTrustDevice();
trusted_devices.forEach((trusted_device) => {
if (trusted_device.is_current_device === 1) {
this.token_expiration = this.trusted_token_expiration;
this.log.debug(`API.addTrustDevice(): This device is trusted. Token expiration extended to: ${this.token_expiration})`);
}
});
return true;
}
Expand Down Expand Up @@ -250,10 +255,10 @@ class API extends events_1.EventEmitter {
dataresult.forEach(element => {
this.log.debug(`API.updateDeviceInfo(): stations - element: ${JSON.stringify(element)}`);
this.log.debug(`API.updateDeviceInfo(): stations - device_type: ${element.device_type}`);
if (element.device_type == 0) {
// Station
this.hubs[element.station_sn] = element;
}
//if (element.device_type == 0) {
// Station
this.hubs[element.station_sn] = element;
//}
});
}
else {
Expand Down Expand Up @@ -322,7 +327,6 @@ class API extends events_1.EventEmitter {
default: break;
}
}
//TODO: It seems that if the device is a trusted device the token doesn't expires as stated by token_expiration. So change this accordingly!
if (this.token_expiration && (new Date()).getTime() >= this.token_expiration.getTime()) {
this.log.info("Access token expired; fetching a new one");
this.invalidateToken();
Expand Down Expand Up @@ -420,6 +424,42 @@ class API extends events_1.EventEmitter {
return false;
});
}
setParameters(station_sn, device_sn, params) {
return __awaiter(this, void 0, void 0, function* () {
const tmp_params = [];
params.forEach(param => {
tmp_params.push({ param_type: param.param_type, param_value: parameter_1.Parameter.writeValue(param.param_type, param.param_value) });
});
try {
const response = yield this.request("post", "app/upload_devs_params", {
device_sn: device_sn,
station_sn: station_sn,
params: tmp_params
}).catch(error => {
this.log.error(`API.setParameters(): error: ${JSON.stringify(error)}`);
return error;
});
this.log.debug(`API.setParameters(): station_sn: ${station_sn} device_sn: ${device_sn} params: ${JSON.stringify(tmp_params)} Response: ${JSON.stringify(response.data)}`);
if (response.status == 200) {
const result = response.data;
if (result.code == 0) {
const dataresult = result.data;
this.log.debug(`API.setParameters(): New Parameters set. response: ${JSON.stringify(dataresult)}`);
return true;
}
else
this.log.error(`API.setParameters(): Response code not ok (code: ${result.code} msg: ${result.msg})`);
}
else {
this.log.error(`API.setParameters(): Status return code not 200 (status: ${response.status} text: ${response.statusText}`);
}
}
catch (error) {
this.log.error(`API.setParameters(): error: ${error}`);
}
return false;
});
}
getLog() {
return this.log;
}
Expand All @@ -435,6 +475,9 @@ class API extends events_1.EventEmitter {
getTokenExpiration() {
return this.token_expiration;
}
getTrustedTokenExpiration() {
return this.trusted_token_expiration;
}
setToken(token) {
this.token = token;
axios_1.default.defaults.headers.common["X-Auth-Token"] = token;
Expand Down
38 changes: 7 additions & 31 deletions build/lib/eufy-security/http/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ class Device extends events_1.EventEmitter {
static isMotionSensor(type) {
return types_1.DeviceType.MOTION_SENSOR == type;
}
static isIntegratedDeviceBySn(sn) {
return sn.startsWith("T8420") || sn.startsWith("T820") || sn.startsWith("T8410") || sn.startsWith("T8400") || sn.startsWith("T8401") || sn.startsWith("T8411") || sn.startsWith("T8130") || sn.startsWith("T8131");
}
static isSoloCameraBySn(sn) {
return sn.startsWith("T8130") || sn.startsWith("T8131");
}
isCamera() {
return Device.isCamera(this.device.device_type);
}
Expand Down Expand Up @@ -264,37 +270,7 @@ class Device extends events_1.EventEmitter {
}
setParameters(params) {
return __awaiter(this, void 0, void 0, function* () {
const tmp_params = [];
params.forEach(param => {
tmp_params.push({ param_type: param.param_type, param_value: parameter_1.Parameter.writeValue(param.param_type, param.param_value) });
});
try {
const response = yield this.api.request("post", "app/upload_devs_params", {
device_sn: this.device.device_sn,
station_sn: this.device.station_sn,
json: tmp_params
}).catch(error => {
this.log.error(`Device.setParameters(): error: ${JSON.stringify(error)}`);
return error;
});
this.log.debug(`Device.setParameters(): Response: ${JSON.stringify(response.data)}`);
if (response.status == 200) {
const result = response.data;
if (result.code == 0) {
const dataresult = result.data;
this.log.debug("New Parameters successfully set.");
this.log.info(`Device.setParameters(): New Parameters set. response: ${JSON.stringify(dataresult)}`);
}
else
this.log.error(`Device.setParameters(): Response code not ok (code: ${result.code} msg: ${result.msg})`);
}
else {
this.log.error(`Device.setParameters(): Status return code not 200 (status: ${response.status} text: ${response.statusText}`);
}
}
catch (error) {
this.log.error(`Device.setParameters(): error: ${error}`);
}
return this.api.setParameters(this.device.station_sn, this.device.device_sn, params);
});
}
getChannel() {
Expand Down
57 changes: 36 additions & 21 deletions build/lib/eufy-security/http/station.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.Station = void 0;
const types_1 = require("./types");
const parameter_1 = require("./parameter");
const utils_1 = require("./utils");
const protocol_1 = require("../p2p/protocol");
const session_1 = require("../p2p/session");
const types_2 = require("../p2p/types");
const utils_1 = require("../p2p/utils");
const utils_2 = require("../p2p/utils");
const events_1 = require("events");
const device_1 = require("./device");
class Station extends events_1.EventEmitter {
constructor(api, hub) {
super();
Expand Down Expand Up @@ -161,7 +163,7 @@ class Station extends events_1.EventEmitter {
let local_addr = null;
for (const addr of addrs) {
this.log.debug("Station.connect(): Discovered station addresses: host: " + addr.host + " port: " + addr.port);
if (utils_1.isPrivateIp(addr.host)) {
if (utils_2.isPrivateIp(addr.host)) {
local_addr = addr;
}
}
Expand Down Expand Up @@ -191,26 +193,39 @@ class Station extends events_1.EventEmitter {
setGuardMode(mode) {
return __awaiter(this, void 0, void 0, function* () {
this.log.silly("Station.setGuardMode(): ");
if (!this.p2p_session || !this.p2p_session.isConnected()) {
this.log.debug(`Station.setGuardMode(): P2P connection to station ${this.getSerial()} not present, establish it.`);
yield this.connect();
}
if (this.p2p_session) {
if (this.p2p_session.isConnected()) {
this.log.debug(`Station.setGuardMode(): P2P connection to station ${this.getSerial()} present, send command mode: ${mode}.`);
yield this.p2p_session.sendCommandWithInt(types_2.CommandType.CMD_SET_ARMING, mode, Station.CHANNEL);
// New method available only after a min. software version and only for some devices; The software version is received by FirebaseRemoteConfig
// if ((b != null && a.a().a("new_instance_vision_as", b.main_sw_version) && !b.isIntegratedDeviceBySn()) || (b != null && b.isSoloCams())) {
// If this is met the following works already:
/*await this.p2p_session.sendCommandWithString(CommandType.CMD_SET_PAYLOAD, JSON.stringify({
"account_id": this.hub.member.action_user_id,
"cmd": CommandType.CMD_SET_ARMING,
"mValue3": 0,
"payload": {
"mode_type": mode,
"user_name": this.hub.member.nick_name
if (this.hub.device_type == types_1.DeviceType.STATION) {
if (!this.p2p_session || !this.p2p_session.isConnected()) {
this.log.debug(`Station.setGuardMode(): P2P connection to station ${this.getSerial()} not present, establish it.`);
yield this.connect();
}
if (this.p2p_session) {
if (this.p2p_session.isConnected()) {
this.log.debug(`Station.setGuardMode(): P2P connection to station ${this.getSerial()} present, send command mode: ${mode}.`);
if ((utils_1.isGreaterMinVersion("2.0.7.9", this.getSerial()) && !device_1.Device.isIntegratedDeviceBySn(this.getSerial())) || device_1.Device.isSoloCameraBySn(this.getSerial())) {
this.log.debug("Station.setGuardMode(): Using CMD_SET_PAYLOAD...");
yield this.p2p_session.sendCommandWithString(types_2.CommandType.CMD_SET_PAYLOAD, JSON.stringify({
"account_id": this.hub.member.action_user_id,
"cmd": types_2.CommandType.CMD_SET_ARMING,
"mValue3": 0,
"payload": {
"mode_type": mode,
"user_name": this.hub.member.nick_name
}
}), Station.CHANNEL);
}
}));*/
else {
this.log.debug("Station.setGuardMode(): Using CMD_SET_ARMING...");
yield this.p2p_session.sendCommandWithInt(types_2.CommandType.CMD_SET_ARMING, mode, Station.CHANNEL);
}
}
}
}
else {
//TODO: Experimental!
this.log.debug(`Station.setGuardMode(): Station ${this.getSerial()} is also a device, try to send command mode with HTTPS: ${mode}.`);
if (yield this.api.setParameters(this.hub.station_sn, this.hub.station_sn, [{ param_type: types_1.ParamType.GUARD_MODE, param_value: mode.toString() }])) {
this.log.debug(`Station.setGuardMode(): Station ${this.getSerial()} is also a device, guard mode changed successfully to: ${mode}.`);
this.emit("parameter", this, types_1.ParamType.GUARD_MODE, mode.toString());
}
}
});
Expand Down
Loading

0 comments on commit c79071e

Please sign in to comment.