Skip to content

Commit

Permalink
Rooms based off area (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zoe authored Apr 27, 2024
1 parent d6a6c99 commit 027c486
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 72 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ dictionaries: []
words:
- hass
- sonarjs
- mireds
ignoreWords: []
import: []
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"name": "@digital-alchemy/automation",
"repository": "https://github.com/Digital-Alchemy-TS/automation",
"homepage": "https://docs.digital-alchemy.app/Automation",
"version": "0.3.5",
"version": "0.3.6",
"scripts": {
"build": "rm -rf dist/; tsc",
"lint": "eslint src",
"test": "./scripts/test.sh",
"prepublishOnly": "npm run build",
"upgrade": "ncu -u; npm i"
"upgrade": "ncu -f '@digital-alchemy/*' -u; npm i"
},
"author": {
"url": "https://github.com/zoe-codez",
Expand All @@ -25,8 +25,8 @@
},
"license": "MIT",
"dependencies": {
"@digital-alchemy/core": "^0.3.11",
"@digital-alchemy/hass": "^0.3.9",
"@digital-alchemy/core": "^0.3.12",
"@digital-alchemy/hass": "^0.3.20",
"@digital-alchemy/synapse": "^0.3.5",
"dayjs": "^1.11.10",
"prom-client": "^15.1.1"
Expand Down
59 changes: 37 additions & 22 deletions src/extensions/aggressive-scenes.extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { each, is, TContext, TServiceParams } from "@digital-alchemy/core";
import { domain, ENTITY_STATE, PICK_ENTITY } from "@digital-alchemy/hass";
import {
domain,
ENTITY_STATE,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
} from "@digital-alchemy/hass";

import {
AGGRESSIVE_SCENES_ADJUSTMENT,
Expand All @@ -9,11 +15,11 @@ import {
SceneSwitchState,
} from "../helpers";

type TValidateOptions = {
type TValidateOptions<ROOM extends TAreaId> = {
context: TContext;
room: string;
name: string;
scene: RoomScene;
scene: RoomScene<ROOM>;
};

export function AggressiveScenes({
Expand All @@ -24,12 +30,14 @@ export function AggressiveScenes({
automation,
}: TServiceParams) {
// eslint-disable-next-line sonarjs/cognitive-complexity
async function manageSwitch(
entity: ENTITY_STATE<PICK_ENTITY<"switch">>,
scene: SceneDefinition,
) {
const entity_id = entity.entity_id as PICK_ENTITY<"switch">;
const expected = scene[entity_id] as SceneSwitchState;
async function manageSwitch<
ROOM extends TAreaId,
SCENE extends SceneDefinition<ROOM>,
>(entity: ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>, scene: SCENE) {
const entity_id = entity.entity_id as PICK_FROM_AREA<ROOM, "switch">;
const expected = scene[
entity_id as Extract<keyof SCENE, PICK_FROM_AREA<ROOM, "switch">>
] as SceneSwitchState;
if (is.empty(expected)) {
// ??
return;
Expand All @@ -43,19 +51,23 @@ export function AggressiveScenes({
}
let performedUpdate = false;
if (entity.state !== expected.state) {
await matchSwitchToScene(entity, expected);
await matchSwitchToScene(
entity as ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>,
expected,
);
performedUpdate = true;
}
if (performedUpdate) {
return;
}

if ("entity_id" in entity.attributes) {
const attributes = entity.attributes as { entity_id: PICK_ENTITY[] };
if ("entity_id" in attributes) {
// ? This is a group
const id = entity.attributes.entity_id;
const id = attributes.entity_id;
if (is.array(id) && !is.empty(id)) {
await each(
entity.attributes.entity_id as PICK_ENTITY<"switch">[],
attributes.entity_id as PICK_ENTITY<"switch">[],
async child_id => {
const child = hass.entity.byId(child_id);
if (!child) {
Expand All @@ -75,16 +87,20 @@ export function AggressiveScenes({
return;
}
if (child.state !== expected.state) {
await matchSwitchToScene(child, expected);
await matchSwitchToScene<ROOM>(
// @ts-expect-error wtf
child as ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>,
expected,
);
}
},
);
}
}
}

async function matchSwitchToScene(
entity: ENTITY_STATE<PICK_ENTITY<"switch">>,
async function matchSwitchToScene<ROOM extends TAreaId>(
entity: ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>,
expected: SceneSwitchState,
) {
const entity_id = entity.entity_id;
Expand All @@ -110,12 +126,12 @@ export function AggressiveScenes({
* - warnings
* - state changes
*/
async function validateRoomScene({
async function validateRoomScene<ROOM extends TAreaId>({
scene,
room,
name,
context,
}: TValidateOptions): Promise<void> {
}: TValidateOptions<ROOM>): Promise<void> {
if (
config.automation.AGGRESSIVE_SCENES === false ||
scene?.aggressive === false
Expand Down Expand Up @@ -160,10 +176,9 @@ export function AggressiveScenes({
);
return;
case "switch":
await manageSwitch(
entity as ENTITY_STATE<PICK_ENTITY<"switch">>,
scene.definition,
);
// @ts-expect-error wtf
const item = entity as ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>;
await manageSwitch(item, scene.definition);
return;
default:
logger.debug(
Expand Down
26 changes: 14 additions & 12 deletions src/extensions/light-manager.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
ENTITY_STATE,
GenericEntityDTO,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
} from "@digital-alchemy/hass";

import { RoomDefinition } from "..";
Expand Down Expand Up @@ -77,11 +79,11 @@ export function LightManager({
*
* Same as RGB only, but will preferentially use color temp mode
*/
async function manageLight(
async function manageLight<ROOM extends TAreaId>(
entity: ENTITY_STATE<PICK_ENTITY<"light">>,
scene: SceneDefinition,
scene: SceneDefinition<ROOM>,
) {
const entity_id = entity.entity_id as PICK_ENTITY<"light">;
const entity_id = entity.entity_id as PICK_FROM_AREA<ROOM, "light">;
const expected = scene[entity_id] as SceneLightState;
if (is.empty(expected)) {
// ??
Expand Down Expand Up @@ -283,15 +285,15 @@ export function LightManager({
// Notice already being emitted from room extension
return [];
}
return Object.keys(room.currentSceneDefinition.definition).filter(
key => {
if (!is.domain(key, "light")) {
return false;
}
// TODO: Introduce additional checks for items like rgb color
return room.currentSceneDefinition.definition[key].state !== "off";
},
);
const keys = Object.keys(current) as (keyof typeof current)[];
return keys.filter(key => {
if (!is.domain(key, "light")) {
return false;
}
const entity = current[key] as { state: string };
// TODO: Introduce additional checks for items like rgb color
return entity.state !== "off";
});
}),
) as PICK_ENTITY<"light">[];

Expand Down
48 changes: 35 additions & 13 deletions src/extensions/room.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@ import {
TServiceParams,
VALUE,
} from "@digital-alchemy/core";
import { ALL_DOMAINS, PICK_ENTITY } from "@digital-alchemy/hass";
import {
ALL_DOMAINS,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
} from "@digital-alchemy/hass";
import { VirtualSensor } from "@digital-alchemy/synapse";

import { RoomConfiguration, RoomScene, SceneLightState } from "..";
import {
RoomConfiguration,
RoomScene,
SceneDefinition,
SceneLightState,
} from "..";

function toHassId<DOMAIN extends ALL_DOMAINS>(
domain: DOMAIN,
Expand All @@ -23,11 +33,15 @@ function toHassId<DOMAIN extends ALL_DOMAINS>(
return `${domain}.${name}` as PICK_ENTITY<DOMAIN>;
}

export type RoomDefinition<SCENES extends string = string> = {
export type RoomDefinition<
SCENES extends string = string,
ROOM extends TAreaId = TAreaId,
> = {
scene: SCENES;
currentSceneDefinition: RoomScene;
currentSceneDefinition: RoomScene<ROOM>;
currentSceneEntity: VirtualSensor<SCENES>;
sceneId: (scene: SCENES) => PICK_ENTITY<"scene">;
name: ROOM;
};
interface HasKelvin {
kelvin: number;
Expand All @@ -42,11 +56,11 @@ export function Room({
context: parentContext,
}: TServiceParams) {
// eslint-disable-next-line sonarjs/cognitive-complexity
return function <SCENES extends string>({
name,
return function <SCENES extends string, ROOM extends TAreaId>({
area: name,
context,
scenes,
}: RoomConfiguration<SCENES>): RoomDefinition<SCENES> {
}: RoomConfiguration<SCENES, ROOM>): RoomDefinition<SCENES> {
logger.info({ name }, `create room`);
const SCENE_LIST = Object.keys(scenes) as SCENES[];

Expand Down Expand Up @@ -107,7 +121,8 @@ export function Room({
if (!is.empty(target) && target !== "on") {
return false;
}
const current = (scenes[currentScene.state as SCENES] ?? {}) as RoomScene;
const current = (scenes[currentScene.state as SCENES] ??
{}) as RoomScene<ROOM>;
const definition = current.definition;
if (entity_id in definition) {
const state = definition[entity_id] as SceneLightState;
Expand All @@ -119,15 +134,18 @@ export function Room({
}

function dynamicProperties(sceneName: SCENES) {
const item = scenes[sceneName] as RoomScene;
const definition = item.definition as Record<
PICK_ENTITY<"light">,
SceneLightState
const { definition } = scenes[sceneName] as RoomScene<
ROOM,
SceneDefinition<ROOM>
>;
const entities = Object.keys(item.definition) as PICK_ENTITY<"light">[];
if (!is.object(definition)) {
return { lights: {}, scene: {} };
}
const entities = Object.keys(definition) as PICK_FROM_AREA<ROOM>[];
const kelvin = automation.circadian.getKelvin();
const list = entities
.map(name => {
// @ts-expect-error wtf
const value = definition[name] as SceneLightState;

if (is.domain(name, "switch")) {
Expand All @@ -142,6 +160,7 @@ export function Room({
return [name, { kelvin, ...value }];
})
.filter(i => !is.undefined(i));

return {
lights: Object.fromEntries(
list.filter(i => !is.undefined((i[VALUE] as HasKelvin).kelvin)),
Expand Down Expand Up @@ -220,6 +239,9 @@ export function Room({
return toHassId("scene", name, scene);
};
}
if (property === "name") {
return name;
}
if (property === "currentSceneEntity") {
return currentScene;
}
Expand Down
Loading

0 comments on commit 027c486

Please sign in to comment.