Skip to content
This repository has been archived by the owner on Oct 20, 2024. It is now read-only.

Commit

Permalink
v3.4.0 (#54)
Browse files Browse the repository at this point in the history
* fix/buffer-error

* wip check power

* cleanup/code

* refactor/hub-child-commands

* refactor/prepare-for-klap-protocol

* feat/contact-sensor

* docs/update

* feat/p110-p115-power-usage

* feat/add-low-battery-button

* version
  • Loading branch information
Nicolae-Rares Ailincai authored Sep 5, 2023
1 parent 6645f44 commit 703eda4
Show file tree
Hide file tree
Showing 17 changed files with 626 additions and 207 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
}
}
},
"cSpell.words": ["Tapo"]
"cSpell.words": ["KLAP", "Tapo"]
}
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ Package was renamed from `homebridge-tplink-smart-light` to `homebridge-tp-link-

### Current device types

- Socket/Outlet
- Socket/Outlet (For devices with power measurement, they have a contact sensor, open means the current is > 0 and closed is 0)
- Hub (As alarm)
- Button S200
- Contact Sensor (T110)
- Light Bulb
- LED Strip

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"private": false,
"displayName": "TPLink Tapo",
"name": "homebridge-tp-link-tapo",
"version": "3.3.0",
"version": "3.4.0",
"description": "A platform to implement the tp-link tapo device and Adaptive Lighting for light bulbs",
"license": "Apache-2.0",
"repository": {
Expand Down
16 changes: 10 additions & 6 deletions src/@types/Accessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export enum AccessoryType {

export enum ChildType {
Unknown = 'Unknown',
Button = 'LightBulb'
Button = 'LightBulb',
Contact = 'Contact'
}

abstract class Accessory {
Expand All @@ -40,11 +41,14 @@ abstract class Accessory {
}

public static GetChildType(deviceInfo: ChildInfo): ChildType {
if (
deviceInfo?.type?.includes('SENSOR') &&
deviceInfo?.category?.includes('button')
) {
return ChildType.Button;
if (deviceInfo?.type?.includes('SENSOR')) {
if (deviceInfo?.category?.includes('button')) {
return ChildType.Button;
}

if (deviceInfo?.category?.includes('contact-sensor')) {
return ChildType.Contact;
}
}

return ChildType.Unknown;
Expand Down
18 changes: 18 additions & 0 deletions src/accessories/Button/characteristics/StatusLowBattery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
CharacteristicGetHandler,
CharacteristicValue,
Nullable
} from 'homebridge';

import { AccessoryThisType } from '..';

const characteristic: {
get: CharacteristicGetHandler;
} & AccessoryThisType = {
get: async function (): Promise<Nullable<CharacteristicValue>> {
const deviceInfo = await this.getInfo();
return deviceInfo.at_low_battery;
}
};

export default characteristic;
18 changes: 18 additions & 0 deletions src/accessories/Button/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import Context from '../../@types/Context';
import Platform from '../../platform';
import delay from '../../utils/delay';

import StatusLowBattery from './characteristics/StatusLowBattery';

export type AccessoryThisType = ThisType<{
readonly hub: HubAccessory;
readonly getInfo: () => Promise<ChildInfo>;
}>;

export default class ButtonAccessory extends Accessory {
private interval?: NodeJS.Timeout;
private lastEventUpdate = 0;
Expand All @@ -15,6 +22,10 @@ export default class ButtonAccessory extends Accessory {
return this.accessory.UUID.toString();
}

private getInfo() {
return this.hub.getChildInfo(this.deviceInfo.device_id);
}

constructor(
private readonly hub: HubAccessory,
platform: Platform,
Expand Down Expand Up @@ -55,6 +66,13 @@ export default class ButtonAccessory extends Accessory {
]
});

(
service.getCharacteristic(
this.platform.Characteristic.StatusLowBattery
) ||
service.addCharacteristic(this.platform.Characteristic.StatusLowBattery)
).onGet(StatusLowBattery.get.bind(this));

const checkStatus = async () => {
try {
const response = await this.hub.getChildLogs(this.deviceInfo.device_id);
Expand Down
18 changes: 18 additions & 0 deletions src/accessories/Contact/characteristics/StatusLowBattery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
CharacteristicGetHandler,
CharacteristicValue,
Nullable
} from 'homebridge';

import { AccessoryThisType } from '..';

const characteristic: {
get: CharacteristicGetHandler;
} & AccessoryThisType = {
get: async function (): Promise<Nullable<CharacteristicValue>> {
const deviceInfo = await this.getInfo();
return deviceInfo.at_low_battery;
}
};

export default characteristic;
114 changes: 114 additions & 0 deletions src/accessories/Contact/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { PlatformAccessory, Logger } from 'homebridge';

import { ChildInfo } from '../../api/@types/ChildListInfo';
import HubAccessory, { HubContext } from '../Hub';
import Accessory from '../../@types/Accessory';
import Context from '../../@types/Context';
import Platform from '../../platform';
import delay from '../../utils/delay';

import StatusLowBattery from './characteristics/StatusLowBattery';

export enum Status {
KeepOpen = 'keepOpen',
Closed = 'close',
Open = 'open'
}

export type AccessoryThisType = ThisType<{
readonly hub: HubAccessory;
readonly getInfo: () => Promise<ChildInfo>;
}>;

export default class ContactAccessory extends Accessory {
private interval?: NodeJS.Timeout;
private lastEventUpdate = 0;

public get UUID() {
return this.accessory.UUID.toString();
}

private getInfo() {
return this.hub.getChildInfo(this.deviceInfo.device_id);
}

constructor(
private readonly hub: HubAccessory,
platform: Platform,
accessory: PlatformAccessory<HubContext>,
log: Logger,
deviceInfo: ChildInfo
) {
super(
platform,
accessory as unknown as PlatformAccessory<Context>,
log,
deviceInfo
);

this.accessory
.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(
this.platform.Characteristic.Manufacturer,
'TP-Link Technologies'
)
.setCharacteristic(this.platform.Characteristic.Model, this.model)
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.mac);

const service =
this.accessory.getService(this.platform.Service.ContactSensor) ||
this.accessory.addService(this.platform.Service.ContactSensor);

const characteristic = service.getCharacteristic(
this.platform.Characteristic.ContactSensorState
);

service
.getCharacteristic(this.platform.Characteristic.StatusLowBattery)
.onGet(StatusLowBattery.get.bind(this));

const checkStatus = async (initStatus?: Status) => {
try {
if (initStatus) {
characteristic.updateValue(this.statusToValue(initStatus));
}

const response = await this.hub.getChildLogs(this.deviceInfo.device_id);
const lastEvent = response?.logs?.[0];
if (this.lastEventUpdate < lastEvent?.timestamp) {
this.lastEventUpdate = lastEvent?.timestamp ?? 0;
characteristic.updateValue(this.statusToValue(lastEvent?.event));
}
} catch (error) {
this.log.error('Failed to check for updates', error);
await delay(500);
}

checkStatus();
};

this.setup((x) => checkStatus(x));
}

cleanup() {
clearInterval(this.interval!);
}

private async setup(callback: (x: Status) => void) {
const init = await this.hub.getChildLogs(this.deviceInfo.device_id);
const initEvent = init?.logs?.[0];
this.lastEventUpdate = initEvent?.timestamp ?? 0;
callback(initEvent?.event ?? Status.KeepOpen);
}

private statusToValue(status: Status) {
switch (status) {
case Status.Open:
case Status.KeepOpen:
return this.platform.Characteristic.ContactSensorState
.CONTACT_NOT_DETECTED;
default:
return this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED;
}
}
}
12 changes: 10 additions & 2 deletions src/accessories/Hub/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ export default class HubAccessory extends Accessory {
return response.child_device_list;
}

public async getChildInfo(childId: string) {
return this.tpLink.getChildInfo(childId);
}

public async getChildLogs(childId: string) {
const response = await this.tpLink.sendCommand('getTriggerLogs', childId);
return response?.responseData?.result?.responses?.[0]?.result;
const response = await this.tpLink.sendHubCommand(
'getTriggerLogs',
childId,
childId
);
return response?.responseData?.result;
}

constructor(
Expand Down
22 changes: 22 additions & 0 deletions src/accessories/Outlet/characteristics/InUse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
CharacteristicGetHandler,
CharacteristicValue,
Nullable
} from 'homebridge';

import { AccessoryThisType } from '..';

const characteristic: {
get: CharacteristicGetHandler;
} & AccessoryThisType = {
get: async function (): Promise<Nullable<CharacteristicValue>> {
const response = await this.tpLink.cacheSendCommand(
this.mac,
'getCurrentPower'
);

return response.current_power > 0;
}
};

export default characteristic;
35 changes: 35 additions & 0 deletions src/accessories/Outlet/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PlatformAccessory, Service, Logger } from 'homebridge';

import InUse from './characteristics/InUse';
import On from './characteristics/On';

import DeviceInfo from '../../api/@types/DeviceInfo';
Expand Down Expand Up @@ -46,5 +47,39 @@ export default class LightBulbAccessory extends Accessory {
.getCharacteristic(this.platform.Characteristic.On)
.onGet(On.get.bind(this))
.onSet(On.set.bind(this));

this.setupAdditionalCharacteristics();
}

private async setupAdditionalCharacteristics() {
const current = this.service.getCharacteristic(
this.platform.Characteristic.ContactSensorState
);

try {
const check = await this.tpLink.sendCommand('getCurrentPower');
if (
check.current_power === undefined ||
check.current_power === null ||
!Number.isFinite(check.current_power)
) {
throw new Error('Not supported');
}

(
current ||
this.service.addCharacteristic(
this.platform.Characteristic.ContactSensorState
)
).onGet(InUse.get.bind(this));

this.log.debug('InUse characteristic supported.');
} catch {
this.log.debug('InUse characteristic not supported, ignoring.');

if (current) {
this.service.removeCharacteristic(current);
}
}
}
}
50 changes: 50 additions & 0 deletions src/api/@types/API.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AxiosResponse } from 'axios';
import { Logger } from 'homebridge';
import crypto from 'crypto';

import TpLinkCipher from '../TpLinkCipher';

abstract class API {
protected readonly terminalUUID: string;

protected loginToken?: string;

constructor(
protected readonly ip: string,
protected readonly email: string,
protected readonly password: string,
protected readonly log: Logger
) {
this.email = TpLinkCipher.toBase64(TpLinkCipher.encodeUsername(this.email));
this.password = TpLinkCipher.toBase64(this.password);
this.terminalUUID = crypto.randomUUID();
}

public abstract login(): Promise<void>;

public abstract setup(): Promise<void>;

public abstract sendRequest(
method: string,
params: {
[key: string]: any;
},
setCookie: boolean
): Promise<AxiosResponse<any, any>>;

public abstract sendSecureRequest(
method: string,
params: {
[key: string]: any;
},
useToken: boolean,
forceHandshake: boolean
): Promise<{
body: any;
response: AxiosResponse<any, any>;
}>;

public abstract needsNewHandshake(): boolean;
}

export default API;
6 changes: 6 additions & 0 deletions src/api/@types/Protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
enum Protocol {
Legacy,
KLAP
}

export default Protocol;
Loading

0 comments on commit 703eda4

Please sign in to comment.