Skip to content

Commit 6949255

Browse files
committed
Homey OAuth WIP
1 parent e1447a5 commit 6949255

File tree

10 files changed

+769
-94
lines changed

10 files changed

+769
-94
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
declare module 'homey-oauth2app' {
2+
import { App, Driver, Device } from 'homey';
3+
4+
export interface OAuth2Token {
5+
access_token: string;
6+
refresh_token?: string;
7+
token_type?: string;
8+
expires_in?: number;
9+
scope?: string;
10+
}
11+
12+
export interface OAuth2Session {
13+
sessionId: string;
14+
configId?: string;
15+
token: OAuth2Token;
16+
}
17+
18+
export class OAuth2Error extends Error {
19+
constructor(message: string, statusCode?: number);
20+
statusCode?: number;
21+
}
22+
23+
export interface OAuth2ClientConfig {
24+
clientId?: string;
25+
clientSecret?: string;
26+
apiUrl?: string;
27+
tokenUrl?: string;
28+
authorizationUrl?: string;
29+
redirectUrl?: string;
30+
scopes?: string[];
31+
allowMultiSession?: boolean;
32+
}
33+
34+
export interface OAuth2RequestOptions {
35+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
36+
path?: string;
37+
query?: Record<string, any>;
38+
json?: any;
39+
body?: any;
40+
headers?: Record<string, string>;
41+
}
42+
43+
export class OAuth2Client {
44+
static API_URL: string;
45+
static TOKEN_URL: string;
46+
static AUTHORIZATION_URL: string;
47+
static SCOPES: string[];
48+
static REDIRECT_URL: string;
49+
static CLIENT_ID: string;
50+
static CLIENT_SECRET?: string;
51+
static TOKEN: typeof OAuth2Token;
52+
static PKCE: boolean;
53+
54+
constructor(config: OAuth2ClientConfig);
55+
56+
// Token management
57+
getToken(): OAuth2Token | null;
58+
setToken(token: OAuth2Token): void;
59+
refreshToken(): Promise<OAuth2Token>;
60+
save(): Promise<void>;
61+
destroy(): Promise<void>;
62+
63+
// HTTP methods
64+
get(options: OAuth2RequestOptions): Promise<any>;
65+
post(options: OAuth2RequestOptions): Promise<any>;
66+
put(options: OAuth2RequestOptions): Promise<any>;
67+
delete(options: OAuth2RequestOptions): Promise<any>;
68+
patch(options: OAuth2RequestOptions): Promise<any>;
69+
70+
// Authorization
71+
getAuthorizationUrl(): string;
72+
getTokenByCode(code: string, codeVerifier?: string): Promise<OAuth2Token>;
73+
74+
// Hooks (meant to be overridden)
75+
onHandleNotOK(args: { body: any; status: number; statusText: string }): Promise<void>;
76+
onHandleResult(args: { result: any }): Promise<any>;
77+
onHandleGetTokenByCode(args: { code: string; codeVerifier?: string }): Promise<OAuth2Token>;
78+
onHandleRefreshToken(args: { refreshToken: string }): Promise<OAuth2Token>;
79+
onRequestToken(body: any): Promise<OAuth2Token>;
80+
onRequestError(args: { error: Error }): Promise<void>;
81+
onShouldRefreshToken(args: { token: OAuth2Token }): boolean;
82+
83+
// Internal methods
84+
_requestToken(args: { body: any }): Promise<OAuth2Token>;
85+
}
86+
87+
export class OAuth2App extends App {
88+
static OAUTH2_DEBUG: boolean;
89+
static OAUTH2_CLIENT: typeof OAuth2Client;
90+
static OAUTH2_MULTI_SESSION: boolean;
91+
static OAUTH2_DRIVERS: string[];
92+
93+
// Lifecycle
94+
onInit(): Promise<void>;
95+
onOAuth2Init(): Promise<void>;
96+
97+
// Debug
98+
enableOAuth2Debug(): void;
99+
disableOAuth2Debug(): void;
100+
101+
// Config management
102+
setOAuth2Config(config: {
103+
configId?: string;
104+
client?: typeof OAuth2Client;
105+
clientId?: string;
106+
clientSecret?: string;
107+
apiUrl?: string;
108+
token?: string;
109+
tokenUrl?: string;
110+
authorizationUrl?: string;
111+
redirectUrl?: string;
112+
scopes?: string[];
113+
allowMultiSession?: boolean;
114+
}): void;
115+
116+
hasConfig(args?: { configId?: string }): boolean;
117+
checkHasConfig(args?: { configId?: string }): void;
118+
getConfig(args?: { configId?: string }): any;
119+
120+
// Client management
121+
hasOAuth2Client(args?: { sessionId?: string; configId?: string }): boolean;
122+
checkHasOAuth2Client(args?: { sessionId?: string; configId?: string }): void;
123+
createOAuth2Client(args?: { sessionId?: string; configId?: string }): OAuth2Client;
124+
deleteOAuth2Client(args?: { sessionId?: string; configId?: string }): void;
125+
getOAuth2Client(args?: { sessionId?: string; configId?: string }): OAuth2Client;
126+
saveOAuth2Client(args: { configId?: string; sessionId: string; client: OAuth2Client }): void;
127+
128+
// Session management
129+
getSavedOAuth2Sessions(): Record<string, OAuth2Session>;
130+
getSavedOAuth2SessionBySessionId(sessionId: string): OAuth2Session | null;
131+
getFirstSavedOAuth2SessionId(): string | null;
132+
getFirstSavedOAuth2Client(): OAuth2Client | null;
133+
tryCleanSession(args: { sessionId: string; configId?: string }): void;
134+
135+
// Hooks
136+
onOAuth2Saved(args: { sessionId: string; configId?: string }): Promise<void>;
137+
onOAuth2Deleted(args: { sessionId: string; configId?: string }): Promise<void>;
138+
onShouldDeleteSession(args: { sessionId: string; configId?: string }): Promise<boolean>;
139+
getOAuth2Devices(args: { sessionId: string; configId?: string }): Promise<OAuth2Device[]>;
140+
}
141+
142+
export class OAuth2Driver extends Driver {
143+
// Lifecycle
144+
onOAuth2Init(): Promise<void>;
145+
146+
// Pairing
147+
onPairListDevices(args: { oAuth2Client: OAuth2Client }): Promise<any[]>;
148+
149+
// OAuth2 helpers
150+
getOAuth2Client(): OAuth2Client;
151+
}
152+
153+
export class OAuth2Device extends Device {
154+
oAuth2Client: OAuth2Client;
155+
156+
// Lifecycle
157+
onOAuth2Init(): Promise<void>;
158+
onOAuth2Deleted(): Promise<void>;
159+
onOAuth2Uninit(): Promise<void>;
160+
161+
// OAuth2 helpers
162+
getOAuth2Client(): OAuth2Client;
163+
}
164+
165+
export class OAuth2Util {
166+
static generateRandomId(): string;
167+
static generateCodeChallenge(codeVerifier: string): string;
168+
static generateCodeVerifier(): string;
169+
}
170+
}

packages/homey/app.ts

Lines changed: 99 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,77 @@
11
"use strict";
22

3-
import Homey from "homey";
3+
import { OAuth2App } from "homey-oauth2app";
44
import { Products, Teslemetry } from "@teslemetry/api";
5+
import TeslemetryOAuth2Client from "./lib/TeslemetryOAuth2Client.js";
6+
7+
export default class TeslemetryApp extends OAuth2App {
8+
// OAuth2App configuration
9+
static OAUTH2_CLIENT = TeslemetryOAuth2Client;
10+
static OAUTH2_DEBUG = true;
11+
static OAUTH2_MULTI_SESSION = false;
12+
static OAUTH2_DRIVERS = ["vehicle", "powerwall", "wall-connector"];
513

6-
export default class TeslemetryApp extends Homey.App {
714
teslemetry?: Teslemetry;
815
products?: Products;
916
private initializationPromise?: Promise<void>;
1017

1118
/**
12-
* onInit is called when the app is initialized.
19+
* onOAuth2Init is called when the OAuth2App is initialized
1320
*/
14-
async onInit() {
15-
this.homey.log("Teslemetry app initializing...");
21+
async onOAuth2Init() {
22+
this.homey.log("Teslemetry OAuth2 app initializing...");
1623

17-
// Register API routes for settings page
24+
// Register API routes for testing (if needed for settings page)
1825
this.homey.api.on(
19-
"test",
20-
(
21-
args: { token: string },
26+
"test_oauth",
27+
async (
28+
args: { sessionId?: string },
2229
callback: (err: Error | null, result?: boolean) => void,
2330
) => {
24-
const teslemetry = new Teslemetry(args.token);
25-
teslemetry.api
26-
.test()
27-
.then(({ response }) => {
28-
callback(null, response);
29-
})
30-
.catch((error) => {
31-
callback(null, false);
32-
});
31+
try {
32+
const sessionId =
33+
args.sessionId || this.getFirstSavedOAuth2SessionId();
34+
if (!sessionId) {
35+
callback(new Error("No OAuth2 session available"));
36+
return;
37+
}
38+
39+
const client = this.getOAuth2Client({ sessionId });
40+
41+
// Test with Teslemetry SDK using access token
42+
const accessToken = await client.getAccessToken();
43+
const teslemetry = new Teslemetry(accessToken);
44+
const result = await teslemetry.api.test();
45+
callback(null, !!result.response);
46+
} catch (error) {
47+
this.homey.error("OAuth test failed:", error);
48+
callback(null, false);
49+
}
3350
},
3451
);
3552

36-
// Listen for settings changes
37-
this.homey.settings.on("set", (key: string) => {
38-
if (key === "access_token") {
39-
this.homey.log("Access token updated, reinitializing...");
40-
this.reinitialize();
41-
}
42-
});
43-
44-
// Initialize the Teslemetry connection
53+
// Initialize the Teslemetry SDK connection using OAuth2 token
4554
await this.initializeTeslemetry();
4655
}
4756

4857
/**
49-
* Initialize Teslemetry connection with current access token
58+
* Initialize Teslemetry connection with OAuth2 token
5059
*/
5160
private async initializeTeslemetry(): Promise<void> {
5261
try {
53-
const accessToken = this.homey.settings.get("access_token") as string;
54-
55-
if (!accessToken) {
62+
// Get the first available OAuth2 session
63+
const sessionId = this.getFirstSavedOAuth2SessionId();
64+
if (!sessionId) {
5665
this.homey.log(
57-
"No access token configured. Please configure in app settings.",
66+
"No OAuth2 session available. User needs to authenticate.",
5867
);
5968
return;
6069
}
6170

62-
this.homey.log("Initializing Teslemetry with access token...");
71+
const client = this.getOAuth2Client({ sessionId });
72+
73+
this.homey.log("Initializing Teslemetry with OAuth2 token...");
74+
const accessToken = await client.getAccessToken();
6375
this.teslemetry = new Teslemetry(accessToken);
6476
this.products = await this.teslemetry.createProducts();
6577

@@ -78,7 +90,29 @@ export default class TeslemetryApp extends Homey.App {
7890
}
7991

8092
/**
81-
* Reinitialize the app when settings change
93+
* Called when OAuth2 session is created or updated
94+
*/
95+
async onOAuth2Saved({ sessionId }: { sessionId: string }) {
96+
this.homey.log(`OAuth2 session saved: ${sessionId}`);
97+
await this.reinitialize();
98+
}
99+
100+
/**
101+
* Called when OAuth2 session is deleted
102+
*/
103+
async onOAuth2Deleted({ sessionId }: { sessionId: string }) {
104+
this.homey.log(`OAuth2 session deleted: ${sessionId}`);
105+
106+
// Clean up Teslemetry connection
107+
if (this.teslemetry) {
108+
this.teslemetry.sse.close();
109+
this.teslemetry = undefined;
110+
this.products = undefined;
111+
}
112+
}
113+
114+
/**
115+
* Reinitialize the app when OAuth2 session changes
82116
*/
83117
private async reinitialize(): Promise<void> {
84118
// Prevent multiple simultaneous initializations
@@ -95,7 +129,7 @@ export default class TeslemetryApp extends Homey.App {
95129
this.products = undefined;
96130
}
97131

98-
// Initialize with new settings
132+
// Initialize with new OAuth2 session
99133
await this.initializeTeslemetry();
100134
} catch (error) {
101135
this.homey.error("Failed to reinitialize:", error);
@@ -128,15 +162,41 @@ export default class TeslemetryApp extends Homey.App {
128162
}
129163

130164
/**
131-
* Check if the app is properly configured
165+
* Check if the app is properly configured with OAuth2
132166
*/
133167
isConfigured(): boolean {
134-
const accessToken = this.homey.settings.get("access_token") as string;
135-
return !!accessToken && !!this.teslemetry && !!this.products;
168+
const sessionId = this.getFirstSavedOAuth2SessionId();
169+
if (!sessionId) return false;
170+
171+
const session = this.getSavedOAuth2SessionBySessionId(sessionId);
172+
return !!(session && session.token && this.teslemetry && this.products);
173+
}
174+
175+
/**
176+
* Get OAuth2 client for API calls
177+
*/
178+
getOAuth2Client({
179+
sessionId,
180+
}: { sessionId?: string } = {}): TeslemetryOAuth2Client {
181+
const actualSessionId = sessionId || this.getFirstSavedOAuth2SessionId();
182+
if (!actualSessionId) {
183+
throw new Error("No OAuth2 session available");
184+
}
185+
return super.getOAuth2Client({
186+
sessionId: actualSessionId,
187+
}) as TeslemetryOAuth2Client;
188+
}
189+
190+
/**
191+
* Get a token function for the Teslemetry SDK
192+
*/
193+
getTokenFunction(sessionId?: string): () => Promise<string> {
194+
const client = this.getOAuth2Client({ sessionId });
195+
return () => client.getAccessToken();
136196
}
137197

138198
async onUninit() {
139-
this.homey.log("Teslemetry app shutting down...");
199+
this.homey.log("Teslemetry OAuth2 app shutting down...");
140200

141201
// Wait for any pending initialization to complete
142202
if (this.initializationPromise) {

packages/homey/drivers/powerwall/driver.compose.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,26 @@
66
"capabilities": ["measure_power", "measure_battery"],
77
"platforms": ["local", "cloud"],
88
"pair": [
9+
{
10+
"id": "login_oauth2",
11+
"template": "login_oauth2"
12+
},
913
{
1014
"id": "list_devices",
11-
"template": "list_devices"
15+
"template": "list_devices",
16+
"navigation": {
17+
"next": "add_devices"
18+
}
19+
},
20+
{
21+
"id": "add_devices",
22+
"template": "add_devices"
23+
}
24+
],
25+
"repair": [
26+
{
27+
"id": "login_oauth2",
28+
"template": "login_oauth2"
1229
}
1330
]
1431
}

0 commit comments

Comments
 (0)