Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement regions #6

Merged
merged 4 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,50 @@
"required": true,
"default": ""
},
"region": {
"title": "Region",
"type": "string",
"required": true,
"default": "eu",
"oneOf": [
{
"title": "Europe",
"enum": [
"eu"
]
},
{
"title": "United States",
"enum": [
"us"
]
},
{
"title": "Australia",
"enum": [
"au"
]
},
{
"title": "Russia",
"enum": [
"ru"
]
},
{
"title": "China",
"enum": [
"cn"
]
},
{
"title": "Israel",
"enum": [
"il"
]
}
]
},
"pollingInterval": {
"title": "Polling interval",
"type": "number",
Expand Down
1 change: 1 addition & 0 deletions src/accessories/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export abstract class ElectroluxAccessoryController {
}

await axiosAppliance.put(`/appliances/${this.appliance.applianceId}/command`, body, {
baseURL: `${this.platform.regionalBaseUrl}/appliance/api/v2`,
headers: {
'Authorization': `Bearer ${this.platform.accessToken}`
}
Expand Down
1 change: 0 additions & 1 deletion src/const/apiKey.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const ACCOUNTS_API_KEY = '4_JZvZObbVWc1YROHF9e6y8A';
// export const ACCOUNTS_GMID = 'gmid.ver4.AcbHz43lRQ.RfoPo2i0QyAYlA7LrpxWT6EV2JLPd5EvVSzEPsTWOM5aPh8UNoRNkEaQnZtGwsv2.0blXENBGLCuNmWpCJKFkmm1LIZ21b5XpxTNFh9t2aJ_mXtFOguWKM1zRJY4izCaV05-DWJg-ZDnBW_nE1JFKxg.sc3';

export const API_KEY = '2AMqwEV5MqVhTKrRCyYfVF8gmKrd2rAmp7cUsfky';
5 changes: 1 addition & 4 deletions src/const/url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
export const SOCIALIZE_API_URL = 'https://socialize.eu1.gigya.com';
export const ACCOUNTS_API_URL = 'https://accounts.eu1.gigya.com';
export const AUTH_API_URL = 'https://api.eu.ocp.electrolux.one/one-account-authorization/api/v1';
export const APPLIANCE_API_URL = 'https://api.eu.ocp.electrolux.one/appliance/api/v2';
export const API_URL = 'https://api.ocp.electrolux.one/one-account-user/api/v1';
10 changes: 10 additions & 0 deletions src/definitions/identityProviders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type IdentityProvidersResponse = Brand[];

type Brand = {
domain: string;
apiKey: string;
brand: string;
httpRegionalBaseUrl: string;
webSocketRegionalBaseUrl: string;
dataCenter: string;
};
1 change: 1 addition & 0 deletions src/definitions/region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Region = 'eu' | 'us' | 'au' | 'ru' | 'cn' | 'il';
113 changes: 68 additions & 45 deletions src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {

import {PLATFORM_NAME, PLUGIN_NAME} from './settings';
import {
axiosApi,
axiosAppliance,
axiosAuth
} from './services/axios';
Expand All @@ -22,12 +23,14 @@ import { ElectroluxAccessoryController } from './accessories/controller';
import { ElectroluxAccessory } from './accessories/accessory';
import fs from 'fs';
import path from 'path';

/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
import { Region } from './definitions/region';
import { IdentityProvidersResponse } from './definitions/identityProviders';

/*
HomebridgePlatform
This class is the main constructor for your plugin, this is where you should
parse the user config and discover/register accessories with Homebridge.
*/
export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic =
Expand All @@ -44,6 +47,8 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
private refreshToken: string | null = null;
tokenExpirationDate: number | null = null;

regionalBaseUrl: string | null = null;

private devicesDiscovered = false;
private pollingInterval: NodeJS.Timeout | null = null;

Expand Down Expand Up @@ -85,10 +90,10 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
});
}

/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
/*
This function is invoked when homebridge restores cached accessories from disk at startup.
It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory: PlatformAccessory<ElectroluxAccessoryController>) {
this.log.info('Loading accessory from cache:', accessory.displayName);

Expand All @@ -97,7 +102,9 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
}

async signIn() {
this.gigya = new Gigya(ACCOUNTS_API_KEY, 'eu1');
const region: Region = this.config.region || 'eu';

this.gigya = new Gigya(ACCOUNTS_API_KEY, `${region}1`);

const storagePath = path.format({
dir: this.api.user.storagePath(),
Expand All @@ -116,15 +123,13 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
this.tokenExpirationDate = data.tokenExpirationDate;

this.log.info('Auth data restored from cache!');
return;
}

this.log.info('Signing in to Electrolux...');

try {
if(!this.uid || !this.oauthToken) {
if(!this.uid || !this.oauthToken || !this.sessionSecret) {
this.log.info('Signing in to Gigya...');

const loginResponse = await this.gigya.accounts.login({
loginID: this.config.email,
password: this.config.password,
Expand All @@ -133,36 +138,51 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
this.uid = loginResponse.UID;
this.oauthToken = loginResponse.sessionInfo?.sessionToken ?? null;
this.sessionSecret = loginResponse.sessionInfo?.sessionSecret ?? null;

this.log.info('Signed in to Gigya!');
}

const jwtResponse = await this.gigya.accounts.getJWT({
targetUID: this.uid,
fields: 'country',
oauth_token: this.oauthToken ?? undefined,
secret: this.sessionSecret ?? undefined
});
if(!this.accessToken || !this.refreshToken || !this.tokenExpirationDate || Date.now() >= this.tokenExpirationDate){
this.log.info('Fetching JWT token...');

const tokenResponse = await axiosAuth.post<TokenResponse>(
'/token',
{
grantType:
'urn:ietf:params:oauth:grant-type:token-exchange',
clientId: 'ElxOneApp',
idToken: jwtResponse.id_token,
scope: ''
},
{
headers: {
'Origin-Country-Code': 'PL'
const jwtResponse = await this.gigya.accounts.getJWT({
targetUID: this.uid,
fields: 'country',
oauth_token: this.oauthToken ?? undefined,
secret: this.sessionSecret ?? undefined
});

const tokenResponse = await axiosAuth.post<TokenResponse>(
'/token',
{
grantType:
'urn:ietf:params:oauth:grant-type:token-exchange',
clientId: 'ElxOneApp',
idToken: jwtResponse.id_token,
scope: ''
},
{
baseURL: `${this.regionalBaseUrl}/one-account-authorization/api/v1`,
headers: {
'Origin-Country-Code': 'PL'
}
}
}
);
);

this.accessToken = tokenResponse.data.accessToken;
this.refreshToken = tokenResponse.data.refreshToken;
this.tokenExpirationDate = Date.now() + tokenResponse.data.expiresIn * 1000;

this.accessToken = tokenResponse.data.accessToken;
this.refreshToken = tokenResponse.data.refreshToken;
this.tokenExpirationDate = Date.now() + tokenResponse.data.expiresIn * 1000;
this.log.info('JWT token successfully fetched!');
}

const regionResponse = await axiosApi.get<IdentityProvidersResponse>('/identity-providers', {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
});

this.log.info('Signed in to Electrolux!');
this.regionalBaseUrl = regionResponse.data.find(({brand}) => brand === 'electrolux')?.httpRegionalBaseUrl ?? null;

const json = JSON.stringify({
uid: this.uid,
Expand Down Expand Up @@ -200,6 +220,8 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {
clientId: 'ElxOneApp',
refreshToken: this.refreshToken,
scope: ''
}, {
baseURL: `${this.regionalBaseUrl}/one-account-authorization/api/v1`
});

this.accessToken = response.data.accessToken;
Expand All @@ -211,18 +233,19 @@ export class ElectroluxDevicesPlatform implements DynamicPlatformPlugin {

private async getAppliances() {
const response = await axiosAppliance.get<Appliances>('/appliances', {
baseURL: `${this.regionalBaseUrl}/appliance/api/v2`,
headers: {
Authorization: `Bearer ${this.accessToken}`
}
});
return response.data;
}

/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
/*
This is an example method showing how to register discovered accessories.
Accessories must only be registered once, previously created accessories
must not be registered again to prevent "duplicate UUID" errors.
*/
async discoverDevices() {
this.log.info('Discovering devices...');

Expand Down
14 changes: 11 additions & 3 deletions src/services/axios.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import axios from 'axios';
import { API_KEY } from '../const/apiKey';
import { APPLIANCE_API_URL, AUTH_API_URL } from '../const/url';
import { API_URL } from '../const/url';

export const axiosApi = axios.create({
baseURL: API_URL,
headers: {
'Accept': 'application/json',
'Accept-Charset': 'utf-8',
'x-api-key': API_KEY,
'User-Agent': 'Ktor client'
}
});

export const axiosAuth = axios.create({
baseURL: AUTH_API_URL,
headers: {
'Accept': 'application/json',
'Accept-Charset': 'utf-8',
Expand All @@ -15,7 +24,6 @@ export const axiosAuth = axios.create({
});

export const axiosAppliance = axios.create({
baseURL: APPLIANCE_API_URL,
headers: {
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Charset': 'utf-8',
Expand Down
38 changes: 0 additions & 38 deletions src/util/signature.ts

This file was deleted.

Loading