dirigera
Version:
A TypeScript client for IKEA's DIRIGERA smart home hub
225 lines (209 loc) • 7.93 kB
text/typescript
import os from 'node:os'
import { Agent } from 'node:https'
import {
calculateCodeChallenge,
CODE_CHALLENGE_METHOD,
generateCodeVerifier,
} from './authCode'
import certificate from './certificate'
import { discoverGatewayIP } from './mdnsDiscovery'
import { closeWebSocket, initializeWebSocket } from './ws'
import type { Home } from './types/Home'
import type { Event } from './types/event/Event'
import hub from './api/hub'
import devices from './api/devices'
import lights from './api/lights'
import outlets from './api/outlets'
import controllers from './api/controllers'
import speakers from './api/speakers'
import blinds from './api/blinds'
import airPurifiers from './api/airPurifiers'
import repeaters from './api/repeaters'
import motionSensors from './api/motionSensors'
import lightSensors from './api/lightSensors'
import environmentSensors from './api/environmentSensors'
import openCloseSensors from './api/openCloseSensors'
import waterSensors from './api/waterSensors'
import deviceSets from './api/deviceSets'
import rooms from './api/rooms'
import scenes from './api/scenes'
import music from './api/music'
import users from './api/users'
export type DirigeraClient = Awaited<ReturnType<typeof createDirigeraClient>>
export type { AirPurifier } from './types/device/AirPurifier'
export type { Blinds } from './types/device/Blinds'
export type { Capabilities } from './types/Capabilities'
export type { Controller } from './types/device/Controller'
export type { Device } from './types/device/Device'
export type { DeviceSet } from './types/DeviceSet'
export type { EnvironmentSensor } from './types/device/EnvironmentSensor'
export type { Home } from './types/Home'
export type { Hub } from './types/device/Hub'
export type { Light } from './types/device/Light'
export type { LightSensor } from './types/device/LightSensor'
export type { MotionSensor } from './types/device/MotionSensor'
export type { Music } from './types/Music'
export type { OpenCloseSensor } from './types/device/OpenCloseSensor'
export type { Outlet } from './types/device/Outlet'
export type { Repeater } from './types/device/Repeater'
export type { Room } from './types/Room'
export type { Scene } from './types/Scene'
export type { Speaker } from './types/device/Speaker'
export type { User } from './types/User'
export type { WaterSensor } from './types/device/WaterSensor'
export type { Event } from './types/event/Event'
export type { DeviceAddedEvent } from './types/event/DeviceAddedEvent'
export type { DeviceConfigurationChangedEvent } from './types/event/DeviceConfigurationChangedEvent'
export type { DeviceDiscoveredEvent } from './types/event/DeviceDiscoveredEvent'
export type { DeviceRemovedEvent } from './types/event/DeviceRemovedEvent'
export type { DeviceSetCreatedEvent } from './types/event/DeviceSetCreatedEvent'
export type { DeviceSetDeletedEvent } from './types/event/DeviceSetDeletedEvent'
export type { DeviceSetUpdatedEvent } from './types/event/DeviceSetUpdatedEvent'
export type { DeviceStateChangedEvent } from './types/event/DeviceStateChangedEvent'
export type { MusicUpdatedEvent } from './types/event/MusicUpdatedEvent'
export type { PingEvent } from './types/event/PingEvent'
export type { PongEvent } from './types/event/PongEvent'
export type { RoomCreatedEvent } from './types/event/RoomCreatedEvent'
export type { RoomDeletedEvent } from './types/event/RoomDeletedEvent'
export type { RoomUpdatedEvent } from './types/event/RoomUpdatedEvent'
export type { SceneCompletedEvent } from './types/event/SceneCompletedEvent'
export type { SceneCreatedEvent } from './types/event/SceneCreatedEvent'
export type { SceneDeletedEvent } from './types/event/SceneDeletedEvent'
export type { SceneTriggeredEvent } from './types/event/SceneTriggeredEvent'
export type { SceneUpdatedEvent } from './types/event/SceneUpdatedEvent'
export type { TerminationEvent } from './types/event/TerminationEvent'
export async function createDirigeraClient({
gatewayIP,
accessToken,
rejectUnauthorized = true,
}: {
gatewayIP?: string
accessToken?: string
rejectUnauthorized?: boolean
} = {}) {
const ip = gatewayIP ?? (await discoverGatewayIP())
const { got } = await import('got')
const gotInstance = got.extend({
prefixUrl: `https://${ip}:8443/v1`,
headers: {
...(accessToken ? { authorization: `Bearer ${accessToken}` } : null),
},
...(rejectUnauthorized
? {
agent: {
https: new Agent({
ca: [certificate],
checkServerIdentity: () => undefined,
}),
},
}
: { https: { rejectUnauthorized: false } }),
hooks: {
beforeError: [
(error) => {
if (
error.code === 'ERR_NON_2XX_3XX_RESPONSE' &&
error?.response?.body
) {
error.name = 'DirigeraError'
error.message = `${error.response.body}`
}
return error
},
],
},
})
return {
async authenticate({ verbose = false }: { verbose?: boolean } = {}) {
const codeVerifier = generateCodeVerifier()
const codeChallenge = calculateCodeChallenge(codeVerifier)
const { code }: { code: string } = await gotInstance
.get(`oauth/authorize`, {
searchParams: {
audience: 'homesmart.local',
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: CODE_CHALLENGE_METHOD,
},
})
.json()
const { default: pRetry } = await import('p-retry')
if (verbose) {
console.log(
'Press the Action Button on the bottom of your Dirigera Hub within 60 seconds.'
)
}
return await pRetry(
async () => {
const { access_token }: { access_token: string } = await gotInstance
.post(`oauth/token`, {
form: {
code,
name: os.hostname(),
grant_type: 'authorization_code',
code_verifier: codeVerifier,
},
})
.json()
if (!access_token) {
throw new Error('Access token is missing.')
}
if (verbose) {
process.stdout.write('\n')
}
return access_token
},
{
retries: 59,
minTimeout: 1000,
factor: 1,
onFailedAttempt: (error) => {
if (verbose) {
process.stdout.write(
`\rTime left: ${String(error.retriesLeft).padStart(2, ' ')} seconds`
)
if (error.retriesLeft === 0) {
process.stdout.write('\n')
}
}
},
}
)
},
async home() {
return await gotInstance.get(`home`).json<Home>()
},
hub: hub(gotInstance),
devices: devices(gotInstance),
lights: lights(gotInstance),
outlets: outlets(gotInstance),
controllers: controllers(gotInstance),
speakers: speakers(gotInstance),
blinds: blinds(gotInstance),
airPurifiers: airPurifiers(gotInstance),
repeaters: repeaters(gotInstance),
motionSensors: motionSensors(gotInstance),
lightSensors: lightSensors(gotInstance),
environmentSensors: environmentSensors(gotInstance),
openCloseSensors: openCloseSensors(gotInstance),
waterSensors: waterSensors(gotInstance),
deviceSets: deviceSets(gotInstance),
rooms: rooms(gotInstance),
scenes: scenes(gotInstance),
music: music(gotInstance),
users: users(gotInstance),
startListeningForUpdates(callback: (updateEvent: Event) => void) {
if (!accessToken) {
throw new Error('Access token is missing.')
}
initializeWebSocket({
ip,
accessToken,
callback,
})
},
stopListeningForUpdates() {
closeWebSocket()
},
}
}