Skip to content

Commit

Permalink
Add homebridge integration (#45)
Browse files Browse the repository at this point in the history
* Add Homebridge integration

* Add light control

* Add homebridge integration online check

* Add Homebridge integration settings

* Add homebridge integration config handle

* Remove debugging messages.
Check accessory supports HSB mode.

* Several refactors related to homebridge

---------

Co-authored-by: Joost <git@jstt.me>
  • Loading branch information
ballinc and JustJoostNL authored Aug 21, 2024
1 parent a72241d commit 66cec59
Show file tree
Hide file tree
Showing 21 changed files with 735 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/main/initIntegrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { globalConfig } from "./ipc/config";
import { registerDiscordRPC } from "./lightController/integrations/discord/api";
import { goveeInitialize } from "./lightController/integrations/govee/api";
import { homeAssistantInitialize } from "./lightController/integrations/homeAssistant/api";
import { homebridgeInitialize } from "./lightController/integrations/homebridge/api";
import { mqttInitialize } from "./lightController/integrations/mqtt/api";
import { openrgbInitialize } from "./lightController/integrations/openrgb/api";
import { philipsHueInitialize } from "./lightController/integrations/philipsHue/api";
Expand All @@ -28,6 +29,11 @@ export async function initializeIntegrations() {
function: homeAssistantInitialize,
enabled: globalConfig.homeAssistantEnabled,
},
{
name: "homebridge",
function: homebridgeInitialize,
enabled: globalConfig.homebridgeEnabled,
},
{
name: "philipsHue",
function: philipsHueInitialize,
Expand Down
11 changes: 11 additions & 0 deletions src/main/ipc/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
homeAssistantCheckDeviceSpectrum,
homeAssistantGetDevices,
} from "../lightController/integrations/homeAssistant/api";
import { homebridgeGetAccessories } from "../lightController/integrations/homebridge/api";
import {
discoverPhilipsHueBridge,
generatePhilipsHueBridgeAuthToken,
Expand All @@ -24,6 +25,11 @@ async function handleHomeAssistantCheckDeviceSpectrum(entityId: string) {
return await homeAssistantCheckDeviceSpectrum(entityId);
}

// homebridge
async function handleHomebridgeGetAccessories() {
return await homebridgeGetAccessories();
}

//philips hue
async function handleDiscoverPhilipsHueBridge() {
return await discoverPhilipsHueBridge();
Expand Down Expand Up @@ -66,6 +72,10 @@ function registerIntegrationsIPCHandlers() {
return handleHomeAssistantCheckDeviceSpectrum(arg);
},
);
ipcMain.handle(
"f1mvli:integrations:homebridge:getAccessories",
handleHomebridgeGetAccessories,
);
ipcMain.handle(
"f1mvli:integrations:philipsHue:discoverBridge",
handleDiscoverPhilipsHueBridge,
Expand Down Expand Up @@ -97,6 +107,7 @@ function registerIntegrationsIPCHandlers() {
ipcMain.removeHandler(
"f1mvli:integrations:homeAssistant:checkDeviceSpectrum",
);
ipcMain.removeHandler("f1mvli:integrations:homebridge:getAccessories");
ipcMain.removeHandler("f1mvli:integrations:philipsHue:discoverBridge");
ipcMain.removeHandler("f1mvli:integrations:philipsHue:generateAuthToken");
ipcMain.removeHandler("f1mvli:integrations:philipsHue:getDevices");
Expand Down
7 changes: 7 additions & 0 deletions src/main/ipc/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { BrowserWindow, app, ipcMain } from "electron";
import { integrationStates } from "../lightController/integrations/states";
import { homeAssistantOnlineCheck } from "../lightController/integrations/homeAssistant/api";
import { homebridgeOnlineCheck } from "../lightController/integrations/homebridge/api";
import { philipsHueOnlineCheck } from "../lightController/integrations/philipsHue/api";
import { tradfriOnlineCheck } from "../lightController/integrations/tradfri/api";
import { getConfig } from "./config";

const handleGetIntegrationStates = async () => {
const config = await getConfig();
if (config.homeAssistantEnabled) await homeAssistantOnlineCheck();
if (config.homebridgeEnabled) await homebridgeOnlineCheck();
if (config.philipsHueEnabled) await philipsHueOnlineCheck();
if (config.ikeaEnabled) await tradfriOnlineCheck();

Expand All @@ -32,6 +34,11 @@ const handleGetIntegrationStates = async () => {
state: integrationStates.homeAssistant,
disabled: !config.homeAssistantEnabled,
},
{
name: "homebridge",
state: integrationStates.homebridge,
disabled: !config.homebridgeEnabled,
},
{
name: "philipsHue",
state: integrationStates.philipsHue,
Expand Down
21 changes: 21 additions & 0 deletions src/main/lightController/controlAllLights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Action, EventType } from "../../shared/config/config_types";
import { getConfig } from "../ipc/config";
import { goveeControl } from "./integrations/govee/api";
import { homeAssistantControl } from "./integrations/homeAssistant/api";
import { homebridgeControl } from "./integrations/homebridge/api";
import { mqttControl } from "./integrations/mqtt/api";
import { openrgbControl } from "./integrations/openrgb/api";
import { philipsHueControl } from "./integrations/philipsHue/api";
Expand Down Expand Up @@ -59,6 +60,14 @@ export async function controlAllLights({
});
}

if (config.homebridgeEnabled) {
await homebridgeControl({
controlType,
color,
brightness,
});
}

if (config.philipsHueEnabled) {
await philipsHueControl({
controlType,
Expand Down Expand Up @@ -141,6 +150,18 @@ export async function turnOffAllLights() {
});
}

if (config.homebridgeEnabled) {
await homebridgeControl({
controlType: ControlType.Off,
color: {
r: 0,
g: 0,
b: 0,
},
brightness: 100,
});
}

if (config.webserverEnabled) {
await webServerControl({
color: {
Expand Down
237 changes: 237 additions & 0 deletions src/main/lightController/integrations/homebridge/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import fetch from "cross-fetch";
import log from "electron-log";
import { getConfig, globalConfig } from "../../../ipc/config";
import { integrationStates } from "../states";
import { ControlType } from "../../controlAllLights";
import { rgbToHueSat } from "../rgbToHueSat";
import {
IHomebridgeAccessory,
IHomebridgeAccessoryResponse,
IHomebridgeAuthCheckResponse,
IHomebridgeTokenResponse,
} from "../../../../shared/integrations/homebridge_types";

let storedToken: { token: string; expiry: number } | null = null;

export async function homebridgeOnlineCheck(): Promise<"online" | "offline"> {
const url = new URL("/api/auth/check", globalConfig.homebridgeHost);
url.port = globalConfig.homebridgePort.toString();

const token = await requestHomebridgeToken();

const options = {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
};

try {
const res = await fetch(url, options);
const data: IHomebridgeAuthCheckResponse = await res.json();

const isOnline = data.status === "OK";

integrationStates.homebridge = isOnline;
return isOnline ? "online" : "offline";
} catch (err) {
integrationStates.homebridge = false;
return "offline";
}
}

export async function requestHomebridgeToken() {
if (storedToken && storedToken.expiry > Date.now()) {
return storedToken.token;
}

const url = new URL("/api/auth/login", globalConfig.homebridgeHost);
url.port = globalConfig.homebridgePort.toString();

const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: globalConfig.homebridgeUsername,
password: globalConfig.homebridgePassword,
}),
};

try {
const res = await fetch(url, options);
const data: IHomebridgeTokenResponse = await res.json();

storedToken = {
token: data.access_token,
expiry: Date.now() + data.expires_in * 1000,
};

return data.access_token;
} catch (err) {
log.error(
`An error occurred while requesting the Homebridge token: ${err}`,
);
}
}

export async function homebridgeInitialize() {
log.debug("Checking if the Homebridge API is online...");

const status = await homebridgeOnlineCheck();

if (status === "online") {
log.debug("Homebridge API is online.");
} else {
log.error(
"Error: Could not connect to the Homebridge API, please make sure that the hostname and port are correct!",
);
}
}

export async function homebridgeGetAccessories() {
const config = await getConfig();
const url = new URL("/api/accessories", config.homebridgeHost);
url.port = config.homebridgePort.toString();

const token = await requestHomebridgeToken();

const options = {
method: "GET",
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
};

const res = await fetch(url, options);
const json: IHomebridgeAccessoryResponse = await res.json();

const lightList: IHomebridgeAccessory[] = [];

json.forEach((item) => {
if (item.type == "Lightbulb" && homebridgeSupportsHSB(item)) {
lightList.push(item);
}
});

return {
accessories: lightList,
selectedAccessories: config.homebridgeAccessories,
};
}

export function homebridgeSupportsHSB(accessory: IHomebridgeAccessory) {
if ("Hue" in accessory.values && "Saturation" in accessory.values) {
return true;
}

return false;
}

interface HomebridgeControlArgs {
controlType: ControlType;
color: {
r: number;
g: number;
b: number;
};
brightness: number;
}

export async function homebridgeControl({
controlType,
color,
brightness,
}: HomebridgeControlArgs) {
if (!integrationStates.homebridge) return;

const config = await getConfig();
const token = await requestHomebridgeToken();

const homebridgeAccessories = config.homebridgeAccessories;

const headers = {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
};

switch (controlType) {
case ControlType.On:
for (const accessory in homebridgeAccessories) {
const uniqueId = homebridgeAccessories[accessory];

const url = new URL(
"/api/accessories/" + uniqueId,
config.homebridgeHost,
);
url.port = config.homebridgePort.toString();

const { hue, sat } = rgbToHueSat(color.r, color.g, color.b);

const putData = [
{
characteristicType: "On",
value: 1,
},
{
characteristicType: "Hue",
value: Math.floor(hue),
},
{
characteristicType: "Saturation",
value: Math.floor(sat),
},
{
characteristicType: "Brightness",
value: brightness,
},
];

putData.forEach(async (data) => {
const options = {
method: "PUT",
headers,
body: JSON.stringify(data),
};

const response = await fetch(url, options);

if (!response.ok) {
log.error(
`An error occurred while turning on Homebridge device ${uniqueId}: ${response.statusText}`,
);
}
});
}
break;
case ControlType.Off:
for (const accessory in homebridgeAccessories) {
const uniqueId = homebridgeAccessories[accessory];

const url = new URL(
"/api/accessories/" + uniqueId,
config.homebridgeHost,
);
url.port = config.homebridgePort.toString();

const options = {
method: "PUT",
headers,
body: JSON.stringify({
characteristicType: "On",
value: 0,
}),
};

try {
await fetch(url, options);
} catch (err) {
log.error(
`An error occurred while turning off Homebridge device ${uniqueId}: ${err}`,
);
}
}
break;
}
}
1 change: 1 addition & 0 deletions src/main/lightController/integrations/states.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const integrationStates = {
autoUpdater: false,
f1tvLiveSession: false,
homeAssistant: false,
homebridge: false,
philipsHue: false,
govee: false,
streamdeck: false,
Expand Down
Loading

0 comments on commit 66cec59

Please sign in to comment.