Skip to content

Commit

Permalink
Support user/pass when accessing Native API
Browse files Browse the repository at this point in the history
  • Loading branch information
KyleBoyer committed Aug 13, 2024
1 parent ffbe13d commit 08f330b
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 23 deletions.
14 changes: 14 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@
"required": false,
"pattern": "^([A-F0-9]{2}:){5}[A-F0-9]{2}$",
"placeholder": "00:00:00:00:00:00"
},
"username": {
"title": "(Optional) Username",
"description": "Username to access the Native API",
"type": "string",
"required": false,
"placeholder": "abc123"
},
"password": {
"title": "(Optional) Password",
"description": "Password to access the Native API",
"type": "string",
"required": false,
"placeholder": "xyz456"
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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": "Konnected BlaQ",
"name": "homebridge-blaq",
"version": "0.2.31",
"version": "0.2.32",
"description": "Control and view your garage door(s) remotely with real-time updates using Konnected's BlaQ hardware",
"license": "Apache-2.0",
"type": "module",
Expand Down
22 changes: 22 additions & 0 deletions src/accessory/base.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CharacteristicValue, Logger, PlatformAccessory, Service, WithUUID } from 'homebridge';
import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen.
import { LogMessageEvent, PingMessageEvent, StateUpdateMessageEvent, StateUpdateRecord } from '../utils/eventsource';
import { BlaQHomebridgePluginPlatform } from '../platform';
import { BlaQTextSensorEvent } from '../types';
import type { RequestInfo, RequestInit } from 'node-fetch';

export interface BaseBlaQAccessoryInterface {
setAPIBaseURL: (apiBaseURL: string) => void;
Expand All @@ -14,6 +16,8 @@ export interface BaseBlaQAccessoryInterface {
export type BaseBlaQAccessoryConstructorParams = {
accessory: PlatformAccessory;
apiBaseURL: string;
apiUser?: string;
apiPass?: string;
friendlyName: string;
platform: BlaQHomebridgePluginPlatform;
serialNumber: string;
Expand All @@ -32,6 +36,8 @@ export const correctAPIBaseURL = (inputURL: string) => {

export class BaseBlaQAccessory implements BaseBlaQAccessoryInterface {
protected apiBaseURL: string;
protected user?: string;
protected pass?: string;
protected firmwareVersion?: string;
protected synced?: boolean;
protected queuedEvents: {
Expand All @@ -49,6 +55,8 @@ export class BaseBlaQAccessory implements BaseBlaQAccessoryInterface {
constructor({
accessory,
apiBaseURL,
apiUser,
apiPass,
friendlyName,
platform,
serialNumber,
Expand All @@ -60,6 +68,8 @@ export class BaseBlaQAccessory implements BaseBlaQAccessoryInterface {
this.friendlyName = friendlyName;
this.serialNumber = serialNumber;
this.apiBaseURL = correctAPIBaseURL(apiBaseURL);
this.user = apiUser;
this.pass = apiPass;
this.accessoryInformationService = this.getOrAddService(this.platform.service.AccessoryInformation);
// set accessory information
this.accessoryInformationService
Expand Down Expand Up @@ -160,4 +170,16 @@ export class BaseBlaQAccessory implements BaseBlaQAccessoryInterface {
setAPIBaseURL(url: string){
this.apiBaseURL = correctAPIBaseURL(url);
}

protected authFetch(url: URL | RequestInfo, init?: RequestInit){
const newInit = init || {};
const basicCreds = `${this.user}:${this.pass}`;
newInit['headers'] = {
...newInit['headers'],
...(this.user && this.pass ? {
'Authorization': `Basic ${Buffer.from(basicCreds).toString('base64')}`,
} : {}),
};
return fetch(url, newInit);
}
}
9 changes: 4 additions & 5 deletions src/accessory/garage-door.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CharacteristicValue, Service } from 'homebridge';
import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen.

import {
BlaQBinarySensorEvent,
Expand Down Expand Up @@ -90,7 +89,7 @@ export class BlaQGarageDoorAccessory extends BaseBlaQAccessory {
const apiTarget: string = lockDesired ? 'lock' : 'unlock';
const currentlyLocked = this.getLockState() === this.platform.characteristic.LockCurrentState.SECURED;
if(lockDesired !== currentlyLocked){
await fetch(`${this.apiBaseURL}/lock/${this.lockType}/${apiTarget}`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/lock/${this.lockType}/${apiTarget}`, {method: 'POST'});
}
}

Expand Down Expand Up @@ -265,7 +264,7 @@ export class BlaQGarageDoorAccessory extends BaseBlaQAccessory {
private async setHoldPositionState(target: CharacteristicValue){
const shouldHold = target;
if(shouldHold){
await fetch(`${this.apiBaseURL}/cover/${this.coverType}/stop`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/cover/${this.coverType}/stop`, {method: 'POST'});
}
}

Expand All @@ -281,7 +280,7 @@ export class BlaQGarageDoorAccessory extends BaseBlaQAccessory {
throw new Error(`Invalid target door state: ${target}`);
}
this.updateCurrentDoorState();
await fetch(`${this.apiBaseURL}/cover/${this.coverType}/${apiTarget}`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/cover/${this.coverType}/${apiTarget}`, {method: 'POST'});
}

getTargetDoorPosition(): CharacteristicValue {
Expand Down Expand Up @@ -313,7 +312,7 @@ export class BlaQGarageDoorAccessory extends BaseBlaQAccessory {
}
this.updateCurrentDoorState();
if(this.position !== roundedTarget){
await fetch(`${this.apiBaseURL}/cover/${this.coverType}/set?position=${roundedTarget / 100}`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/cover/${this.coverType}/set?position=${roundedTarget / 100}`, {method: 'POST'});
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/accessory/garage-learn-mode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CharacteristicValue, Service } from 'homebridge';
import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen.

import {
BlaQButtonEvent,
Expand Down Expand Up @@ -47,7 +46,7 @@ export class BlaQGarageLearnModeAccessory extends BaseBlaQAccessory {
private async changeIsOn(target: CharacteristicValue){
const apiTarget: string = target ? 'turn_on' : 'turn_off';
if(target !== this.isOn){ // only call the API when target = true (button on)
await fetch(`${this.apiBaseURL}/switch/learn/${apiTarget}`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/switch/learn/${apiTarget}`, {method: 'POST'});
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/accessory/garage-light.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CharacteristicValue, Service } from 'homebridge';
import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen.

import {
BlaQButtonEvent,
Expand Down Expand Up @@ -52,7 +51,7 @@ export class BlaQGarageLightAccessory extends BaseBlaQAccessory {
private async changePowerState(target: CharacteristicValue){
const apiTarget: string = target ? 'turn_on' : 'turn_off';
if(target !== this.isOn){
await fetch(`${this.apiBaseURL}/light/${this.lightType}/${apiTarget}`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/light/${this.lightType}/${apiTarget}`, {method: 'POST'});
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/accessory/garage-lock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CharacteristicValue, Service } from 'homebridge';
import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen.

import {
BlaQButtonEvent,
Expand Down Expand Up @@ -65,7 +64,7 @@ export class BlaQGarageLockAccessory extends BaseBlaQAccessory {
const lockDesired = target === this.platform.characteristic.LockTargetState.SECURED;
const apiTarget: string = lockDesired ? 'lock' : 'unlock';
if(lockDesired !== this.isLocked){
await fetch(`${this.apiBaseURL}/lock/${this.lockType}/${apiTarget}`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/lock/${this.lockType}/${apiTarget}`, {method: 'POST'});
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/accessory/garage-pre-close-warning.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { CharacteristicValue, Service } from 'homebridge';
import fetch from 'node-fetch'; // I am, in fact, trying to make fetch happen.

import {
BlaQButtonEvent,
Expand Down Expand Up @@ -46,7 +45,7 @@ export class BlaQGaragePreCloseWarningAccessory extends BaseBlaQAccessory {

private async changeIsOn(target: CharacteristicValue){
if(target && target !== this.isOn){ // only call the API when target = true (button on)
await fetch(`${this.apiBaseURL}/button/pre-close_warning/press`, {method: 'POST'});
await this.authFetch(`${this.apiBaseURL}/button/pre-close_warning/press`, {method: 'POST'});
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class BlaQHub {
private friendlyName?: string;
private deviceMac?: string;
private port: number;
private user?: string;
private pass?: string;
private eventsBeforeAccessoryInit: {
type: 'state' | 'log' | 'ping';
event: StateUpdateMessageEvent | LogMessageEvent | PingMessageEvent;
Expand All @@ -62,6 +64,8 @@ export class BlaQHub {
logger.debug('Initializing BlaQHub...');
this.host = configDevice.host;
this.port = configDevice.port;
this.user = configDevice.username;
this.pass = configDevice.password;
this.initAccessoryCallback = initAccessoryCallback;
this.logger = logger;
this.reinitializeEventSource();
Expand All @@ -85,6 +89,8 @@ export class BlaQHub {
this.eventSource = new AutoReconnectingEventSource({
host: this.host,
port: this.port,
user: this.user,
pass: this.pass,
logger: this.logger,
onStateUpdate: (stateEvent) => this.handleStateUpdate(stateEvent),
onLog: (logEvent) => this.handleLogUpdate(logEvent),
Expand Down Expand Up @@ -182,13 +188,17 @@ export class BlaQHub {
private initGarageDoorAccessory({ platform, accessory, friendlyName, serialNumber}: InitAccessoryParams){
this.accessories.push(new BlaQGarageDoorAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(), type: this.pluginConfig.garageDoorType,
apiUser: this.user,
apiPass: this.pass,
}));
}

private initGarageLightAccessory({ platform, accessory, friendlyName, serialNumber}: InitAccessoryParams){
if(this.pluginConfig.enableLight ?? true) {
this.accessories.push(new BlaQGarageLightAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(),
apiUser: this.user,
apiPass: this.pass,
}));
}
}
Expand All @@ -197,6 +207,8 @@ export class BlaQHub {
if(this.pluginConfig.enableLockRemotes ?? true){
this.accessories.push(new BlaQGarageLockAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(),
apiUser: this.user,
apiPass: this.pass,
}));
}
}
Expand All @@ -205,6 +217,8 @@ export class BlaQHub {
if(this.pluginConfig.enableMotionSensor ?? true){
this.accessories.push(new BlaQGarageMotionSensorAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(),
apiUser: this.user,
apiPass: this.pass,
}));
}
}
Expand All @@ -213,6 +227,8 @@ export class BlaQHub {
if(this.pluginConfig.enablePreCloseWarning ?? true){
this.accessories.push(new BlaQGaragePreCloseWarningAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(),
apiUser: this.user,
apiPass: this.pass,
}));
}
}
Expand All @@ -221,6 +237,8 @@ export class BlaQHub {
if(this.pluginConfig.enableLearnMode ?? true){
this.accessories.push(new BlaQGarageLearnModeAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(),
apiUser: this.user,
apiPass: this.pass,
}));
}
}
Expand All @@ -229,6 +247,8 @@ export class BlaQHub {
if(this.pluginConfig.enableSeparateObstructionSensor ?? true){
this.accessories.push(new BlaQGarageObstructionSensorAccessory({
platform, accessory, friendlyName, serialNumber, apiBaseURL: this.getAPIBaseURL(),
apiUser: this.user,
apiPass: this.pass,
}));
}
}
Expand Down
33 changes: 29 additions & 4 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
import { ConfigDevice } from './types.js';
import { formatMAC } from './utils/formatters.js';

const maskPassword = (d: ConfigDevice) => {
if(d.password){
return {
...d,
password: '***',
};
}
return d;
};

/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
Expand Down Expand Up @@ -77,6 +87,21 @@ export class BlaQHomebridgePluginPlatform implements DynamicPlatformPlugin {
}
}

possiblyMergeWithManualConfigDevice(deviceToMerge: ConfigDevice){
let manualConfigDevice = {};
for (const configDevice of this.config.devices as ConfigDevice[]) {
const matchingMAC = configDevice.mac && formatMAC(configDevice.mac) === formatMAC(deviceToMerge.mac);
const matchingHost = configDevice.host && configDevice.host.toLowerCase() === deviceToMerge.host.toLowerCase();
if(matchingMAC || matchingHost){
manualConfigDevice = configDevice;
}
}
return {
...manualConfigDevice,
...deviceToMerge,
};
}

searchBonjour(){
this.bonjourInstance.find({
type: 'konnected',
Expand All @@ -89,13 +114,13 @@ export class BlaQHomebridgePluginPlatform implements DynamicPlatformPlugin {
service.txt?.project_name?.toLowerCase()?.includes('garage') ||
service.txt?.project_name?.toLowerCase()?.includes('gdo');
if(service.txt?.web_api === 'true' && isGarageProject){
const configEntry: ConfigDevice = {
const configEntry: ConfigDevice = this.possiblyMergeWithManualConfigDevice({
host: service.addresses?.[0] || service.host,
port: service.port,
displayName: service.txt?.friendly_name,
mac: formatMAC(service.txt?.mac),
};
this.logger.debug(`Discovered device via mDNS: ${JSON.stringify(configEntry)}`);
});
this.logger.debug(`Discovered device via mDNS: ${JSON.stringify(maskPassword(configEntry))}`);
this.possiblyRegisterNewDevice(configEntry);
}
});
Expand All @@ -110,7 +135,7 @@ export class BlaQHomebridgePluginPlatform implements DynamicPlatformPlugin {
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
setInterval(() => this.searchBonjour(), FIVE_MINUTES_IN_MS);
for (const configDevice of this.config.devices as ConfigDevice[]) {
this.logger.debug(`Discovered device via manual config: ${JSON.stringify(configDevice)}`);
this.logger.debug(`Discovered device via manual config: ${JSON.stringify(maskPassword(configDevice))}`);
this.possiblyRegisterNewDevice(configDevice);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ export type ConfigDevice = {
displayName: string;
host: string;
port: number;
mac ? : string;
mac?: string;
username?: string;
password?: string;
};
export type GarageLockType = 'lock' | 'lock_remotes';
export type GarageLightType = 'garage_light' | 'light';
Expand Down
Loading

0 comments on commit 08f330b

Please sign in to comment.