Skip to content

Commit

Permalink
Time functions (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zoe authored May 16, 2024
1 parent 027c486 commit f934140
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 79 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,7 @@ export function ExampleRoom({ automation, context }: TServiceParams) {

// check sun position
if (automation.solar.isBetween("dawn", "dusk")) {

// create some reference points with dayjs
const [PM530, NOW] = automation.utils.shortTime(["PM5:30", "NOW"]);
return NOW.isBefore(PM530);
return automation.time.isBefore("PM5:30")
}
return false;
},
Expand Down
49 changes: 26 additions & 23 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@digital-alchemy/automation",
"repository": "https://github.com/Digital-Alchemy-TS/automation",
"homepage": "https://docs.digital-alchemy.app/Automation",
"version": "0.3.6",
"version": "0.3.7",
"scripts": {
"build": "rm -rf dist/; tsc",
"lint": "eslint src",
Expand All @@ -25,8 +25,8 @@
},
"license": "MIT",
"dependencies": {
"@digital-alchemy/core": "^0.3.12",
"@digital-alchemy/hass": "^0.3.20",
"@digital-alchemy/core": "^0.3.15",
"@digital-alchemy/hass": "^0.3.25",
"@digital-alchemy/synapse": "^0.3.5",
"dayjs": "^1.11.10",
"prom-client": "^15.1.1"
Expand Down
6 changes: 3 additions & 3 deletions src/automation.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
Room,
SequenceWatcher,
SolarCalculator,
Time,
} from "./extensions";
import { Utils } from "./extensions/utils.extension";

export const LIB_AUTOMATION = CreateLibrary({
configuration: {
Expand Down Expand Up @@ -73,7 +73,7 @@ export const LIB_AUTOMATION = CreateLibrary({
depends: [LIB_HASS, LIB_SYNAPSE],
name: "automation",
// light depends circadian
priorityInit: ["utils", "circadian"],
priorityInit: ["time", "circadian"],
services: {
/**
* # Aggressive Scenes extension
Expand Down Expand Up @@ -117,7 +117,7 @@ export const LIB_AUTOMATION = CreateLibrary({
/**
* Helper functions
*/
utils: Utils,
time: Time,
},
});

Expand Down
5 changes: 4 additions & 1 deletion src/extensions/aggressive-scenes.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export function AggressiveScenes({
return;
}

const attributes = entity.attributes as { entity_id: PICK_ENTITY[] };
// TODO: FIXME
const attributes = entity.attributes as unknown as {
entity_id: PICK_ENTITY[];
};
if ("entity_id" in attributes) {
// ? This is a group
const id = attributes.entity_id;
Expand Down
1 change: 1 addition & 0 deletions src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./managed-switch.extension";
export * from "./room.extension";
export * from "./sequence-matcher.extension";
export * from "./solar-calc.extension";
export * from "./time.extension";
40 changes: 23 additions & 17 deletions src/extensions/light-manager.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ByIdProxy,
ENTITY_STATE,
GenericEntityDTO,
HassEntityContext,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
Expand All @@ -27,22 +28,27 @@ import {
} from "../helpers";

type ColorModes = "color_temp" | "xy" | "brightness";
export type ColorLight = GenericEntityDTO<{
brightness: number;
color_mode: ColorModes;
color_temp: number;
color_temp_kelvin: number;
entity_id?: PICK_ENTITY<"light">[];
hs_color: [h: number, s: number];
max_color_temp_kelvin: number;
max_mireds: number;
min_color_temp_kelvin: number;
min_mireds: number;
rgb_color: [number, number, number];
supported_color_modes: ColorModes[];
supported_features: number;
xy_color: [x: number, y: number];
}>;
export type ColorLight = GenericEntityDTO<
{
brightness: number;
color_mode: ColorModes;
color_temp: number;
color_temp_kelvin: number;
entity_id?: PICK_ENTITY<"light">[];
hs_color: [h: number, s: number];
max_color_temp_kelvin: number;
max_mireds: number;
min_color_temp_kelvin: number;
min_mireds: number;
rgb_color: [number, number, number];
supported_color_modes: ColorModes[];
supported_features: number;
xy_color: [x: number, y: number];
},
string,
HassEntityContext,
"light"
>;
// const MAX_DIFFERENCE = 100;

type DiffList = {
Expand Down Expand Up @@ -293,7 +299,7 @@ export function LightManager({
const entity = current[key] as { state: string };
// TODO: Introduce additional checks for items like rgb color
return entity.state !== "off";
});
}) as PICK_ENTITY<"light">[];
}),
) as PICK_ENTITY<"light">[];

Expand Down
1 change: 0 additions & 1 deletion src/extensions/room.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export function 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 Down
109 changes: 86 additions & 23 deletions src/extensions/solar-calc.extension.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {
CronExpression,
is,
TBlackHole,
TContext,
TServiceParams,
} from "@digital-alchemy/core";
import { HassConfig } from "@digital-alchemy/hass";
import dayjs, { Dayjs } from "dayjs";
import {
Duration,
DurationUnitsObjectType,
DurationUnitType,
} from "dayjs/plugin/duration";
import EventEmitter from "events";

import { calcSolNoon, calcSunriseSet } from "..";
Expand Down Expand Up @@ -44,14 +49,52 @@ const degreesBelowHorizon = {
twilight: 6,
};
const UNLIMITED = 0;
type Part<CHAR extends string> = `${number}${CHAR}` | "";
type ISO_8601_PARTIAL =
| `${Part<"H" | "h">}${Part<"M" | "m">}${Part<"S" | "s">}`
| "";

export type OffsetTypes =
| Duration
| number
| DurationUnitsObjectType
| ISO_8601_PARTIAL
| [quantity: number, unit: DurationUnitType];

type TOffset = OffsetTypes | (() => OffsetTypes);

type OnSolarEvent = {
label?: string;
/**
* **Any quantity may be negative**
*
* Value must be:
* - (`number`) `ms`
* - (`tuple`) [`quantity`, `unit`]
* - (`string`) `ISO 8601` duration string: `P(#Y)(#M)(#D)(T(#H)(#M)(#S))`
* - (`object`) mapping of units to quantities
* - (`Duration`) `dayjs.duration` object
* - (`function`) a function that returns any of the above
* ---
* Offset calculated at midnight & init
*/
offset?: TOffset;
eventName: SolarEvents;
exec: () => TBlackHole;
};

type SolarReference = Record<SolarEvents, Dayjs> & {
isBetween: (a: SolarEvents, b: SolarEvents) => boolean;
loaded: boolean;
onEvent: (options: OnSolarEvent) => TBlackHole;
};

/**
* Benefits from a persistent cache, like Redis
*/
export function SolarCalculator({
logger,
cache,
internal,
scheduler,
hass,
lifecycle,
Expand Down Expand Up @@ -190,35 +233,55 @@ export function SolarCalculator({
return now.isBetween(solarReference[a], solarReference[b]);
};

function getNextTime(eventName: SolarEvents, offset: TOffset, label: string) {
let duration: Duration;
// * if function, unwrap
if (is.function(offset)) {
offset = offset();
logger.trace({ eventName, label, offset }, `resolved offset`);
}
// * if tuple, resolve
if (is.array(offset)) {
const [amount, unit] = offset;
duration = dayjs.duration(amount, unit);
// * resolve objects, or capture Duration
} else if (is.object(offset)) {
duration = isDuration(offset)
? (offset as Duration)
: dayjs.duration(offset as DurationUnitsObjectType);
}
// * resolve from partial ISO 8601
if (is.string(offset)) {
duration = dayjs.duration(`PT${offset.toUpperCase()}`);
}
// * ms
if (is.number(offset)) {
duration = dayjs.duration(offset, "ms");
}
return duration
? solarReference[eventName].add(duration)
: solarReference[eventName];
}

solarReference.onEvent = ({
context,
eventName,
label,
exec,
offset,
}: OnSolarEvent) => {
event.on(eventName, async () => {
await internal.safeExec({
duration: undefined,
errors: undefined,
exec: async () => await exec(),
executions: undefined,
labels: { context, label },
});
scheduler.sliding({
exec: async () => await exec(),
label,
next: () => getNextTime(eventName, offset, label),
reset: CronExpression.EVERY_DAY_AT_MIDNIGHT,
});
};

return solarReference as SolarReference;
}

type OnSolarEvent = {
context: TContext;
label?: string;
eventName: SolarEvents;
exec: () => TBlackHole;
};

type SolarReference = Record<SolarEvents, Dayjs> & {
isBetween: (a: SolarEvents, b: SolarEvents) => boolean;
loaded: boolean;
onEvent: (options: OnSolarEvent) => TBlackHole;
};
function isDuration(
item: Duration | DurationUnitsObjectType,
): item is Duration {
return typeof item.days === "function";
}
Loading

0 comments on commit f934140

Please sign in to comment.