Skip to content

Commit

Permalink
remove tokens when invalid
Browse files Browse the repository at this point in the history
  • Loading branch information
JeroenVdb committed Jan 7, 2025
1 parent acf24e7 commit 28692fb
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 15 deletions.
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## [2.8.0-beta.0] - 2025-01-07

In this update the plugin will remove the `.daikin-controller-cloud-tokenset` file containing your oauth credentials as soon as the refresh token is marked as invalidated. This prevents people having to manually remove the file when the refresh token is invalidated. A restart of Homebridge is required after the file is removed to restart the authorisation flow.

### Added

- Remove TokenSet file when refresh token is invalidated

## [2.7.0] - 2024-10-22

### Added
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 @@
"displayName": "Daikin Cloud",
"platformname": "daikincloud",
"name": "homebridge-daikin-cloud",
"version": "2.7.0",
"version": "2.8.0-beta.0",
"description": "Integrate with the Daikin Cloud to control your Daikin air conditioning via the cloud",
"license": "Apache-2.0",
"repository": {
Expand Down
70 changes: 56 additions & 14 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {daikinAlthermaAccessory} from './daikinAlthermaAccessory';
import {resolve} from 'node:path';
import {DaikinCloudDevice} from 'daikin-controller-cloud/dist/device';
import {StringUtils} from './utils/strings';
import {OnectaClientConfig} from 'daikin-controller-cloud/dist/onecta/oidc-utils';

import fs from 'node:fs';

const ONE_SECOND = 1000;
const ONE_MINUTE = ONE_SECOND * 60;
Expand All @@ -35,22 +38,37 @@ export class DaikinCloudPlatform implements DynamicPlatformPlugin {
public readonly config: PlatformConfig,
public readonly api: API,
) {
this.log.debug('[Platform] Finished initializing platform:', this.config.name);
this.log.info('--- Daikin info for debugging reasons (enable Debug Mode for more logs) ---');

this.log.debug('[Platform] Initializing platform:', this.config.name);

this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.storagePath = api.user.storagePath();
this.updateIntervalDelay = ONE_MINUTE * (this.config.updateIntervalInMinutes || 15);
this.controller = new DaikinCloudController({
const daikinCloudControllerConfig: OnectaClientConfig = {
oidcClientId: this.config.clientId,
oidcClientSecret: this.config.clientSecret,
oidcCallbackServerBindAddr: this.config.oidcCallbackServerBindAddr,
oidcCallbackServerExternalAddress: this.config.callbackServerExternalAddress,
oidcCallbackServerPort: this.config.callbackServerPort,
oidcTokenSetFilePath: resolve(this.storagePath, '.daikin-controller-cloud-tokenset'),
oidcAuthorizationTimeoutS: 60 * 5,
};

this.log.debug('[Config] Homebridge config', this.getPrivacyFriendlyConfig(this.config));
this.log.debug('[Config] DaikinCloudController config', this.getPrivacyFriendlyOnectaClientConfig(daikinCloudControllerConfig));


fs.stat(daikinCloudControllerConfig.oidcTokenSetFilePath!, (err, stats) => {
if (err) {
this.log.debug('[Config] DaikinCloudController config, oidcTokenSetFile does NOT YET exist, expect a message to start the authorisation flow');
} else {
this.log.debug(`[Config] DaikinCloudController config, oidcTokenSetFile does exist, last modified: ${stats.mtime}, created: ${stats.birthtime}`);
}
});

this.controller = new DaikinCloudController(daikinCloudControllerConfig);

this.api.on('didFinishLaunching', async () => {
this.controller.on('authorization_request', (url) => {
Expand All @@ -68,8 +86,15 @@ export class DaikinCloudPlatform implements DynamicPlatformPlugin {
this.log.debug(`[Rate Limit] Remaining calls today: ${rateLimitStatus.remainingDay}/${rateLimitStatus.limitDay} -- this minute: ${rateLimitStatus.remainingMinute}/${rateLimitStatus.limitMinute}`);
});

await this.discoverDevices(this.controller);
this.startUpdateDevicesInterval();
const onInvalidGrantError = () => this.onInvalidGrantError(daikinCloudControllerConfig);
const devices: DaikinCloudDevice[] = await this.discoverDevices(this.controller, onInvalidGrantError);

if (devices.length > 0) {
await this.createDevices(devices);
this.startUpdateDevicesInterval();
}

this.log.info('--------------- End Daikin info for debugging reasons --------------------');
});
}

Expand All @@ -78,22 +103,23 @@ export class DaikinCloudPlatform implements DynamicPlatformPlugin {
this.accessories.push(accessory);
}

private async discoverDevices(controller: DaikinCloudController) {
let devices: DaikinCloudDevice[] = [];

this.log.info('--- Daikin info for debugging reasons (enable Debug Mode for more logs) ---');

this.log.debug('[Config] User config', this.getPrivacyFriendlyConfig(this.config));

private async discoverDevices(controller: DaikinCloudController, onInvalidGrantError: () => void): Promise<DaikinCloudDevice[]> {
try {
devices = await controller.getCloudDevices();
return await controller.getCloudDevices();
} catch (error) {
if (error instanceof Error) {
error.message = `[API Syncing] Failed to get cloud devices from Daikin Cloud: ${error.message}`;
this.log.error(error.message);

if (error.message.includes('invalid_grant')) {
onInvalidGrantError();
}
}
return [];
}
}

private async createDevices(devices: DaikinCloudDevice[]) {
devices.forEach(device => {
try {
const uuid = this.api.hap.uuid.generate(device.getId());
Expand Down Expand Up @@ -144,8 +170,6 @@ export class DaikinCloudPlatform implements DynamicPlatformPlugin {
}
}
});

this.log.info('--------------- End Daikin info for debugging reasons --------------------');
}

private async updateDevices() {
Expand Down Expand Up @@ -187,4 +211,22 @@ export class DaikinCloudPlatform implements DynamicPlatformPlugin {
excludedDevicesByDeviceId: config.excludedDevicesByDeviceId ? config.excludedDevicesByDeviceId.map(deviceId => StringUtils.mask(deviceId)) : [],
};
}

private getPrivacyFriendlyOnectaClientConfig(config: OnectaClientConfig): object {
return {
...config,
oidcClientId: StringUtils.mask(config.oidcClientId),
oidcClientSecret: StringUtils.mask(config.oidcClientSecret),
};
}

private onInvalidGrantError(daikinCloudControllerConfig: OnectaClientConfig) {
this.log.warn('[API Syncing] TokenSet is invalid, removing TokenSet file');
try {
fs.unlinkSync(daikinCloudControllerConfig.oidcTokenSetFilePath!);
this.log.warn('[API Syncing] TokenSet file is removed, restart Homebridge to restart the authorisation flow');
} catch (e) {
this.log.error('[API Syncing] TokenSet file could not be removed, remove it manually. Location: ', daikinCloudControllerConfig.oidcTokenSetFilePath, e);
}
}
}
8 changes: 8 additions & 0 deletions test/platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ test('Initialize platform', async () => {
});

test('DaikinCloudPlatform with new Aircondition accessory', (done) => {
spyOn(Path, 'resolve').mockImplementation((...args) => {
return args.join('/')
});

// @ts-ignore
jest.spyOn(DaikinCloudController.prototype, 'getCloudDevices').mockResolvedValue([{
getId: () => 'MOCK_ID',
Expand Down Expand Up @@ -76,6 +80,10 @@ test('DaikinCloudPlatform with new Aircondition accessory', (done) => {
});

test('DaikinCloudPlatform with new Altherma accessory', (done) => {
spyOn(Path, 'resolve').mockImplementation((...args) => {
return args.join('/')
});

// @ts-ignore
jest.spyOn(DaikinCloudController.prototype, 'getCloudDevices').mockResolvedValue([{
getId: () => 'MOCK_ID',
Expand Down

0 comments on commit 28692fb

Please sign in to comment.