src/api.ts
import type {
IBaseStationRoot,
IElementRoot,
IEventRoot,
IEventsItem,
IGigasetElementsSystemHealth,
} from "./model";
import { RequestBase, url, urlParams } from "./requestHelper";
import { EndpointError } from "./errors";
import { getSafeTimestampString } from "./utils";
export interface IGigasetElementsApiOptions {
/** GE cloud login email address */
email: string;
/** GE cloud login password */
password: string;
/** automatically (re-) authorize against api after X hours */
authorizeHours?: number;
/** logging function for raw api requests */
requestLogger?: (message: string) => void;
}
/** Authorization decorator automatically (re-)authorizes before api requests if required */
function Authorize(
target: GigasetElementsApi,
title: string,
descriptor: PropertyDescriptor,
) {
const orig = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const api = this as unknown as GigasetElementsApi;
if (api.needsAuth()) await api.authorize();
try {
return await orig.apply(this, args);
} catch (err) {
if (err instanceof EndpointError && err.statusCode === 401) {
api.requestLogger(
"Caught 401 in AuthorizeDecorator - calling authorize and retrying",
);
await api.authorize();
return await orig.apply(this, args);
}
throw err;
}
};
}
/** Class for interfacing with GE cloud api */
export class GigasetElementsApi extends RequestBase {
private readonly options: IGigasetElementsApiOptions;
private nextAuth: number | undefined = undefined;
public constructor(options: IGigasetElementsApiOptions) {
super(options.requestLogger);
// set default options/copy config object
this.options = {
authorizeHours: 6,
...options,
};
// check options
if (options.authorizeHours !== undefined && options.authorizeHours < 0)
throw new Error("authorizeHours may not be a negative number");
// initialize nextAuth
if (options.authorizeHours) this.nextAuth = Date.now() - 1;
}
/** whether GE cloud is in maintenance mode and currently not available */
public async isMaintenance() {
const result = await this.get<{ isMaintenance: boolean }>(url.status);
return result.isMaintenance;
}
/**
* Authorize against the GE cloud. Retrieves and stores authorization cookie for further api requests
*/
public async authorize() {
await this.post(url.login, {
form: {
email: this.options.email,
password: this.options.password,
},
});
await this.get(url.auth);
if (this.nextAuth)
this.nextAuth =
Date.now() + (this.options.authorizeHours || 0) * 60 * 60 * 1000;
return true;
}
/**
* Whether authorization is due
*/
public needsAuth() {
return !!this.nextAuth && Date.now() >= this.nextAuth;
}
/**
* Retrive system health data.
* Automatically handles authorization if required.
*/
@Authorize
public getSystemHealth(): Promise<IGigasetElementsSystemHealth> {
return this.get<IGigasetElementsSystemHealth>(url.health);
}
/**
* Retrieves base station and sensor data.
* Automatically handles authorization if required.
*/
@Authorize
public getBaseStations(): Promise<IBaseStationRoot> {
return this.get<IBaseStationRoot>(url.basestations);
}
/**
* Retrieves elements, including sensor data (i.e. temperature for universal sensor).
* Automatically handles authorization if required.
*/
@Authorize
public getElements(): Promise<IElementRoot> {
return this.get<IElementRoot>(url.elements);
}
/**
* Retrieves the most recent events that occured until a given point in time.
* Events are sorted by timestamp in descending order.
* Only the most recent *limit* number of events will be returned.
* @param until date back to when to retrieve events
* @param limit (optional) number of items to retrieve, default 500
*/
@Authorize
public getRecentEvents(
until: Date | number,
limit = 500,
): Promise<IEventRoot> {
const params = new URLSearchParams();
params.set(urlParams.events.from, getSafeTimestampString(until));
if (limit && limit > 0)
params.set(urlParams.events.limit, getSafeTimestampString(limit));
return this.get<IEventRoot>(url.events + "?" + params.toString());
}
/**
* Retrieves events that occured during a time period.
* Events are sorted by timestamp in descending order.
* Limit is applied from the end of the period.
* @param from date back to when to retrieve events
* @param to date from when on retrieve events
* @param limit (optional) number of items to retrieve, default 500
*/
@Authorize
public getEvents(
from: Date | number,
to: Date | number,
limit = 500,
): Promise<IEventRoot> {
const params = new URLSearchParams();
params.set(urlParams.events.from, getSafeTimestampString(from));
params.set(urlParams.events.to, getSafeTimestampString(to));
if (limit && limit > 0)
params.set(urlParams.events.limit, getSafeTimestampString(limit));
return this.get<IEventRoot>(url.events + "?" + params.toString());
}
/**
* Utility method to retrieves all events that occured during a time period,
* using multiple requests if more than *batchSize* events occured in this period.
* Events are sorted by timestamp in descending order.
* @param from date back to when to retrieve events
* @param to (optional) date from when backwards to retrieve events, defaults to now
* @param batchSize (optional) number of items to retrieve during each request, default/max 500
*/
@Authorize
public async getAllEvents(
from: Date | number,
to?: Date | number,
batchSize = 500,
): Promise<IEventsItem[]> {
if (!to) to = new Date().valueOf();
if (batchSize > 500) batchSize = 500;
let allEvents: IEventsItem[] = [];
let result: IEventRoot;
// load items in multiple batches, in reverse order
do {
result = await this.getEvents(from, to, batchSize);
if (result.events?.length > 0) {
allEvents = allEvents.concat(result.events);
to =
Number.parseInt(result.events[result.events.length - 1].ts, 10) - 1;
}
} while (result.events?.length === batchSize);
return allEvents;
}
/**
* Sends a command
* @param baseStationId id of the base station
* @param endNodeId id of the end node
* @param name name of the command, i.e. "on" or "off" for plugs
*/
@Authorize
public async sendCommand(
baseStationId: string,
endNodeId: string,
name: string,
): Promise<void> {
await this.post(url.cmd(baseStationId, endNodeId), { body: { name } });
}
/**
* Update the thermostat set point
* @param baseStationId id of the base station
* @param mode alarm mode to set
*/
@Authorize
public async setThermostat(
baseStationId: string,
endNodeId: string,
setPoint: number,
): Promise<void> {
await this.put(url.thermostat(baseStationId, endNodeId), {
body: { setPoint },
});
}
/**
* Turn user alarm (panic button) on or off
* @param on whether to turn the alarm on or off
*/
@Authorize
public async setUserAlarm(on: boolean): Promise<void> {
if (on)
await this.post(url.webFrontendSink, {
body: { action: "alarm.user.start" },
});
else await this.delete(url.userAlarm);
}
/**
* Updates the active alarm mode.
* @param baseStationId id of the base station
* @param mode alarm mode to set
*/
@Authorize
public async setAlarmMode(
baseStationId: string,
mode: "away" | "home" | "night" | "custom",
): Promise<void> {
await this.post(url.basestations + "/" + baseStationId, {
body: { intrusion_settings: { active_mode: mode } },
});
}
}