Skip to content

Commit

Permalink
Merge pull request #1 from kovapatrik/develop
Browse files Browse the repository at this point in the history
feat: setStatus, ui
  • Loading branch information
kovapatrik authored Apr 12, 2024
2 parents e102809 + 8a2efe5 commit a362ec1
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 75 deletions.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto eol=lf
36 changes: 25 additions & 11 deletions config.schema.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
{
"pluginAlias": "blueair-purifier",
"pluginType": "platform",
"customUi": true,
"singular": true,
"schema": {
"type": "object",
"properties": {
"username": {
"title": "Username",
"description": "Username for BlueAir account",
"type": "string",
"required": true
"description": "Username for BlueAir account. This should be filled in autometically in the discovery process.",
"type": "string"
},
"password": {
"title": "Password",
"description": "Password for BlueAir account",
"description": "Password for BlueAir account. This should be filled in autometically in the discovery process.",
"type": "string"
},
"region": {
"title": "Region",
"description": "Region for BlueAir account. This should be filled in autometically in the discovery process.",
"type": "string",
"enum": ["Default (all other regions)", "Australia", "China", "Russia", "USA"],
"default": "Default (all other regions)",
"required": true
},
"accountUuid": {
"title": "Account UUID",
"description": "Account UUID for BlueAir account. This should be filled in autometically in the discovery process.",
"type": "string"
},
"verboseLogging": {
"title": "Verbose Logging",
"description": "Enable to receive detailed log messages. Useful for troubleshooting.",
"type": "boolean"
},
"uiDebug": {
"title": "UI Debug",
"description": "Enable to show debug information in the Homebridge UI.",
"type": "boolean"
},
"devices": {
"title": "Devices",
"type": "array",
Expand All @@ -38,11 +55,6 @@
"type": "string",
"required": true
},
"isAWSDevice": {
"title": "AWS Device",
"description": "Enable AWS integration. This is required for devices like DustMagnet, HealthProtect, and Blue Pure.",
"type": "boolean"
},
"led": {
"title": "LED",
"description": "Toggles if the LED switch service is created with the accessory.",
Expand Down Expand Up @@ -95,7 +107,10 @@
"items": [
"username",
"password",
"verboseLogging"
"region",
"accountUuid",
"verboseLogging",
"uiDebug"
]
},
{
Expand All @@ -105,7 +120,6 @@
"items": [
"devices[].name",
"devices[].id",
"devices[].enableAWS",
"devices[].led",
"devices[].airQualitySensor",
"devices[].co2Sensor",
Expand Down
9 changes: 5 additions & 4 deletions homebridge-ui/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ <h5 class="mb-0 text-center">
<option value="China">China</option>
<option value="Russia">Russia</option>
<option value="USA">USA</option>
</select>
</div>
<button class="btn btn-primary btn-login" type="submit" id="discoverBtn" style="border: 0;">
Discover Devices on account
Expand All @@ -42,9 +43,9 @@ <h5 class="mb-0 text-center">
<table class="table table-sm" id="discoverTable">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Id</th>
<th scope="col">Mac</th>
<th scope="col">Account UUID</th>
<th scope="col">Add&nbsp;/&nbsp;Update</th>
</tr>
</thead>
Expand Down Expand Up @@ -180,7 +181,7 @@ <h5 class="mb-0 text-center">

console.info(`Request login...`);
try {
const devices = await homebridge.request('/disover', { username, password, region });
const devices = await homebridge.request('/discover', { username, password, region });
debugLog(`Discovered devices:\n${JSON.stringify(devices, null, 2)}`);
const table = document.getElementById('discoverTable').getElementsByTagName('tbody')[0];
table.innerHTML = '';
Expand All @@ -206,11 +207,11 @@ <h5 class="mb-0 text-center">
const state = initialStates.find((s) => s.id === device.uuid);
const tr = table.insertRow();
const td = tr.insertCell();
td.appendChild(document.createTextNode(device.uuid));
td.appendChild(document.createTextNode(state.name));
td.setAttribute('scope', 'row');

tr.insertCell().appendChild(document.createTextNode(device.uuid));
tr.insertCell().appendChild(document.createTextNode(device.mac));
tr.insertCell().appendChild(document.createTextNode(device.name));

const addCell = tr.insertCell();
if (currentConfig.devices.find((d) => d.id === device.uuid)) {
Expand Down
4 changes: 3 additions & 1 deletion homebridge-ui/server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { HomebridgePluginUiServer, RequestError } = require('@homebridge/plugin-ui-utils');
const { defaultConfig, defaultDeviceConfig } = require('../dist/platformUtils.js');
const { BlueAirAwsApi } = require('../dist/api/BlueAirAwsApi.js').default;
const BlueAirAwsApi = require('../dist/api/BlueAirAwsApi.js').default;

var _ = require('lodash');

Expand Down Expand Up @@ -96,6 +96,7 @@ class UiServer extends HomebridgePluginUiServer {
return devices;
} catch (e) {
const msg = e instanceof Error ? e.stack : e;
this.logger.error(`Device discovery failed:\n${msg}`);
throw new RequestError(`Device discovery failed:\n${msg}`);
}
});
Expand All @@ -105,6 +106,7 @@ class UiServer extends HomebridgePluginUiServer {
return await this.api.getDeviceStatus(accountUuid, uuids);
} catch (e) {
const msg = e instanceof Error ? e.stack : e;
this.logger.error(`Failed to get initial device states:\n${msg}`);
throw new RequestError(`Failed to get initial device states:\n${msg}`);
}
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Homebridge BlueAir Platform",
"name": "homebridge-blueair-purifier",
"version": "0.1.0",
"version": "0.1.1",
"description": "Homebridge plugin for BlueAir purifiers",
"license": "Apache-2.0",
"repository": {
Expand Down
56 changes: 33 additions & 23 deletions src/accessory/AirPurifierAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,49 +23,57 @@ export class AirPurifierAccessory {

this.service.setCharacteristic(this.platform.Characteristic.Name, this.configDev.name);
this.service.getCharacteristic(this.platform.Characteristic.Active)
.on('set', this.setActive.bind(this))
.on('get', this.getActive.bind(this));
.onGet(this.getActive.bind(this))
.onSet(this.setActive.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.CurrentAirPurifierState)
.on('get', this.getCurrentAirPurifierState.bind(this));
.onGet(this.getCurrentAirPurifierState.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.TargetAirPurifierState)
.on('set', this.setTargetAirPurifierState.bind(this))
.on('get', this.getTargetAirPurifierState.bind(this));
.onGet(this.getTargetAirPurifierState.bind(this))
.onSet(this.setTargetAirPurifierState.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.LockPhysicalControls)
.on('set', this.setLockPhysicalControls.bind(this))
.on('get', this.getLockPhysicalControls.bind(this));
.onGet(this.getLockPhysicalControls.bind(this))
.onSet(this.setLockPhysicalControls.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed)
.on('set', this.setRotationSpeed.bind(this))
.on('get', this.getRotationSpeed.bind(this));
.onGet(this.getRotationSpeed.bind(this))
.onSet(this.setRotationSpeed.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.FilterChangeIndication)
.on('get', this.getFilterChangeIndication.bind(this));
// this.service.getCharacteristic(this.platform.Characteristic.FilterChangeIndication)
// .onGet(this.getFilterChangeIndication.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.FilterLifeLevel)
.on('get', this.getFilterLifeLevel.bind(this));
// this.service.getCharacteristic(this.platform.Characteristic.FilterLifeLevel)
// .onGet(this.getFilterLifeLevel.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
.on('get', this.getCurrentTemperature.bind(this));
// this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
// .onGet(this.getCurrentTemperature.bind(this));

// this.service.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)
// .onGet(this.getCurrentRelativeHumidity.bind(this));

this.service.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity)
.on('get', this.getCurrentRelativeHumidity.bind(this));
}

getActive(): CharacteristicValue {
return this.device.state.standby ? this.platform.Characteristic.Active.INACTIVE : this.platform.Characteristic.Active.ACTIVE;
return this.device.state.standby ?
this.platform.Characteristic.Active.INACTIVE :
this.platform.Characteristic.Active.ACTIVE;
}

setActive(value: CharacteristicValue) {
this.device.setState('standby', value === this.platform.Characteristic.Active.ACTIVE);
this.device.setState('standby', value === this.platform.Characteristic.Active.INACTIVE);
}

getCurrentAirPurifierState(): CharacteristicValue {
return this.device.state.standby ?
this.platform.Characteristic.CurrentAirPurifierState.PURIFYING_AIR :
this.platform.Characteristic.CurrentAirPurifierState.IDLE;

if (this.device.state.standby) {
return this.platform.Characteristic.CurrentAirPurifierState.INACTIVE;
}

return this.device.state.automode && this.device.state.fanspeed === 0 ?
this.platform.Characteristic.CurrentAirPurifierState.IDLE :
this.platform.Characteristic.CurrentAirPurifierState.PURIFYING_AIR;
}

getTargetAirPurifierState(): CharacteristicValue {
Expand All @@ -89,7 +97,9 @@ export class AirPurifierAccessory {
}

getRotationSpeed(): CharacteristicValue {
return this.device.state.fanspeed || 0;
return this.device.state.standby === false ?
this.device.state.fanspeed || 0:
0;
}

setRotationSpeed(value: CharacteristicValue) {
Expand Down
33 changes: 25 additions & 8 deletions src/api/BlueAirAwsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export default class BlueAirAwsApi {
) {
const config = BLUEAIR_CONFIG[RegionMap[region]].awsConfig;

this.logger.debug(`Creating BlueAir API instance with config: ${JSON.stringify(config)} and username: ${username}\
and region: ${region}`);

this.gigyaApi = new GigyaApi(username, password, region, logger);
this.blueairAxios = axios.create({
baseURL: `https://${config.restApiId}.execute-api.${config.awsRegion}.amazonaws.com/prod/c`,
Expand All @@ -98,17 +101,22 @@ export default class BlueAirAwsApi {

async login(): Promise<void> {

this.logger.debug('Logging in...');

const { token, secret } = await this.gigyaApi.getGigyaSession();
const { jwt } = await this.gigyaApi.getGigyaJWT(token, secret);
const { accessToken } = await this.getAwsAccessToken(jwt);

this.last_login = Date.now();
this.blueairAxios.defaults.headers['Authorization'] = `Bearer ${accessToken}`;
this.blueairAxios.defaults.headers['idtoken'] = accessToken;

this.logger.debug('Logged in');
}

async checkTokenExpiration(): Promise<void> {
if (LOGIN_EXPIRATION < Date.now() - this.last_login) {
this.logger.debug('Token expired, logging in again');
return await this.login();
}
return;
Expand All @@ -117,7 +125,9 @@ export default class BlueAirAwsApi {
async getDevices(): Promise<BlueAirDeviceDiscovery[]> {
await this.checkTokenExpiration();

const response = await this.apiCall('/registered-devices', undefined);
this.logger.debug('Getting devices...');

const response = await this.apiCall('/registered-devices', undefined, 'GET');

if (!response.data.devices) {
throw new Error('getDevices error: no devices in response');
Expand Down Expand Up @@ -152,9 +162,9 @@ export default class BlueAirAwsApi {
return acc;
}, {} as BlueAirDeviceSensorData),
state: device.states.reduce((acc, state) => {
if (state.v) {
if (state.v !== undefined) {
acc[state.n] = state.v;
} else if (state.vb) {
} else if (state.vb !== undefined) {
acc[state.n] = state.vb;
} else {
this.logger.warn(`getDeviceStatus: unknown state ${JSON.stringify(state)}`);
Expand Down Expand Up @@ -184,13 +194,14 @@ export default class BlueAirAwsApi {
throw new Error(`setDeviceStatus: unknown value type ${typeof value}`);
}

await this.apiCall(`/${uuid}/a/${state}`, body);
const response = await this.apiCall(`/${uuid}/a/${state}`, body);
this.logger.debug(`setDeviceStatus response: ${JSON.stringify(response.data)}`);
}

private async getAwsAccessToken(jwt: string): Promise<{accessToken: string}> {
this.logger.debug('Getting AWS access token...');

const response = await this.apiCall('/login', undefined, {
const response = await this.apiCall('/login', undefined, 'POST', {
'Authorization': `Bearer ${jwt}`,
'idtoken': jwt,
});
Expand All @@ -209,20 +220,26 @@ export default class BlueAirAwsApi {
private async apiCall<T = any>(
url: string,
data?: string | object,
method = 'POST',
headers?: object,
retries = 3,
): Promise<AxiosResponse<T>> {
try {
const response = await this.blueairAxios.post<T>(url, data, { headers });
const response = await this.blueairAxios.request<T>({
url,
method,
data,
headers,
});
if (response.status !== 200) {
throw new Error(`API call error with status ${response.status}: ${response.statusText}, ${JSON.stringify(response.data)}`);
}
return response;
} catch (error) {
if (retries > 0) {
return this.apiCall(url, data, headers, retries - 1);
return this.apiCall(url, data, method, headers, retries - 1);
} else {
throw new Error(`API call failed after ${retries} retries`);
throw new Error(`API call failed after ${3 - retries} retries with error: ${error}`);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/api/Consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ export const BLUEAIR_CONFIG = Object.values(RegionMap).reduce((acc, region: stri
},
}), {} as APIConfig);


export const LOGIN_EXPIRATION = 3600 * 1000 * 24; // n hours in milliseconds
export const BLUEAIR_API_TIMEOUT = 1000 * 5; // n seconds in milliseconds
export const BLUEAIR_API_TIMEOUT = 5 * 1000; // n seconds in milliseconds

export type BlueAirDeviceStatusResponse = {
deviceInfo: {
Expand Down
Loading

0 comments on commit a362ec1

Please sign in to comment.