Skip to content

Commit

Permalink
sync
Browse files Browse the repository at this point in the history
  • Loading branch information
zoe-codez committed May 17, 2024
1 parent 20fa961 commit 024d431
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 77 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ words:
- hass
- gotify
- desync
- datetime
- homeassistant
- zeroconf
- endregion
Expand Down
63 changes: 59 additions & 4 deletions src/extensions/sensor.extension.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
import { TBlackHole, TContext, TServiceParams } from "@digital-alchemy/core";

import { SensorDeviceClasses, TRegistry } from "..";
import { SensorDeviceClasses, SensorStateClass, TRegistry } from "..";

// type SensorTypes =
// | "none"
// | "date"
// | "datetime"
// | "decimal"
// | "float"
// | "int"
// | "string";

type TSensor<STATE extends SensorValue, ATTRIBUTES extends object = object> = {
context: TContext;
defaultState?: STATE;
icon?: string;
defaultAttributes?: ATTRIBUTES;
name: string;
} & SensorDeviceClasses;
/**
* The number of decimals which should be used in the sensor's state when it's displayed.
*/
suggested_display_precision?: number;
/**
* The time when an accumulating sensor such as an electricity usage meter, gas meter, water meter etc. was initialized.
*
* If the time of initialization is unknown, set it to `None`.
*
* Note that the `datetime.datetime` returned by the `last_reset` property will be converted to an ISO 8601-formatted string when the entity's state attributes are updated. When changing `last_reset`, the `state` must be a valid number.
*/
last_reset?: Date;
} & SensorDeviceClasses &
(
| {
/**
* In case this sensor provides a textual state, this property can be used to provide a list of possible states.
* Requires the enum device class to be set.
* Cannot be combined with `state_class` or `native_unit_of_measurement`.
*/
options?: string[];
}
| {
/**
* Type of state. If not `None`, the sensor is assumed to be numerical and will be displayed as a line-chart in the frontend instead of as discrete values.
*/
state_class?: SensorStateClass;
}
);

type SensorConfiguration = {
//
};
type SensorValue = string | number;
type SwitchUpdateCallback<
STATE extends SensorValue = SensorValue,
Expand All @@ -19,16 +59,22 @@ type SwitchUpdateCallback<
export type VirtualSensor<
STATE extends SensorValue = SensorValue,
ATTRIBUTES extends object = object,
CONFIGURATION extends SensorConfiguration = SensorConfiguration,
> = {
icon: string;
attributes: ATTRIBUTES;
_configuration?: CONFIGURATION;
_rawAttributes?: ATTRIBUTES;
name: string;
onUpdate: (callback: SwitchUpdateCallback<STATE, ATTRIBUTES>) => void;
state: STATE;
/**
* bumps the last reset time
*/
reset: () => TBlackHole;
} & SensorDeviceClasses;

export function Sensor({ context, synapse }: TServiceParams) {
export function Sensor({ context, synapse, logger }: TServiceParams) {
const registry = synapse.registry.create<VirtualSensor>({
context,
details: entity => ({
Expand All @@ -44,6 +90,7 @@ export function Sensor({ context, synapse }: TServiceParams) {
function create<
STATE extends SensorValue = SensorValue,
ATTRIBUTES extends object = object,
CONFIGURATION extends SensorConfiguration = SensorConfiguration,
>(entity: TSensor<STATE, ATTRIBUTES>) {
const sensorOut = new Proxy({} as VirtualSensor<STATE, ATTRIBUTES>, {
// ### Getters
Expand All @@ -66,6 +113,13 @@ export function Sensor({ context, synapse }: TServiceParams) {
if (property === "_rawAttributes") {
return loader.attributes;
}
if (property === "reset") {
return function () {
// what it means to "reset" is up to dev
entity.last_reset = new Date();
logger.debug(`reset`);
};
}
if (property === "attributes") {
return new Proxy({} as ATTRIBUTES, {
get: <KEY extends Extract<keyof ATTRIBUTES, string>>(
Expand Down Expand Up @@ -119,12 +173,13 @@ export function Sensor({ context, synapse }: TServiceParams) {
// ## Validate a good id was passed, and it's the only place in code that's using it
const id = registry.add(sensorOut);

const loader = synapse.storage.loader<STATE, ATTRIBUTES>({
const loader = synapse.storage.loader<STATE, ATTRIBUTES, CONFIGURATION>({
id,
name: entity.name,
registry: registry as TRegistry<unknown>,
value: {
attributes: {} as ATTRIBUTES,
configuration: {} as CONFIGURATION,
state: "" as STATE,
},
});
Expand Down
62 changes: 45 additions & 17 deletions src/extensions/storage.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,34 @@ import {
} from "..";
import { TRegistry } from ".";

type StorageData<STATE, ATTRIBUTES> = {
state?: STATE;
type StorageData<STATE, ATTRIBUTES, CONFIGURATION> = {
attributes?: ATTRIBUTES;
configuration?: CONFIGURATION;
state?: STATE;
};
type ValueLoader = <STATE, ATTRIBUTES>(
type ValueLoader = <STATE, ATTRIBUTES, CONFIGURATION>(
id: string,
registry: TRegistry,
) => Promise<StorageData<STATE, ATTRIBUTES>>;
) => Promise<StorageData<STATE, ATTRIBUTES, CONFIGURATION>>;

type LoaderOptions<STATE, ATTRIBUTES extends object> = {
type LoaderOptions<
STATE,
ATTRIBUTES extends object,
CONFIGURATION extends object,
> = {
registry: TRegistry<unknown>;
id: TSynapseId;
name: string;
value: StorageData<STATE, ATTRIBUTES>;
value: StorageData<STATE, ATTRIBUTES, CONFIGURATION>;
};

type TCallback<STATE, ATTRIBUTES extends object> = (
new_state: StorageData<STATE, ATTRIBUTES>,
old_state: StorageData<STATE, ATTRIBUTES>,
type TCallback<
STATE,
ATTRIBUTES extends object,
CONFIGURATION extends object,
> = (
new_state: StorageData<STATE, ATTRIBUTES, CONFIGURATION>,
old_state: StorageData<STATE, ATTRIBUTES, CONFIGURATION>,
) => TBlackHole;

export function ValueStorage({
Expand All @@ -49,11 +58,11 @@ export function ValueStorage({
context,
config,
}: TServiceParams) {
// #MARK: file storage init
lifecycle.onPostConfig(() => {
if (config.synapse.STORAGE !== "file") {
return;
}
// #region file storage init
// APPLICATION_IDENTIFIER > app name
const APP_NAME = internal.boot.application.name;

Expand Down Expand Up @@ -101,7 +110,6 @@ export function ValueStorage({
"BAD_STORAGE_FILE",
`${file} is not a valid file storage target`,
);
// #endregion
}, STORAGE_BOOTSTRAP_PRIORITY);

// #MARK: Loaders
Expand Down Expand Up @@ -129,12 +137,12 @@ export function ValueStorage({
CacheLoader,
HassLoader,

loader<STATE, ATTRIBUTES extends object>({
loader<STATE, ATTRIBUTES extends object, CONFIGURATION extends object>({
registry,
id,
name,
value,
}: LoaderOptions<STATE, ATTRIBUTES>) {
}: LoaderOptions<STATE, ATTRIBUTES, CONFIGURATION>) {
// #MARK: value init
const domain = registry.domain;
lifecycle.onBootstrap(async () => {
Expand All @@ -160,13 +168,17 @@ export function ValueStorage({
});
}

const callbacks = [] as TCallback<STATE, ATTRIBUTES>[];
const callbacks = [] as TCallback<STATE, ATTRIBUTES, CONFIGURATION>[];

function RunCallbacks(data: StorageData<STATE, ATTRIBUTES>) {
// #MARK: RunCallbacks
function RunCallbacks(
data: StorageData<STATE, ATTRIBUTES, CONFIGURATION>,
) {
setImmediate(async () => {
await store();
const current = {
attributes: entity.attributes,
configuration: entity.configuration,
state: entity.state,
};
await registry.send(id, current);
Expand All @@ -183,9 +195,10 @@ export function ValueStorage({
// #MARK: storage commands
const entity = {
attributes: value.attributes,
configuration: value.configuration,

onUpdate() {
return (callback: TCallback<STATE, ATTRIBUTES>) => {
return (callback: TCallback<STATE, ATTRIBUTES, CONFIGURATION>) => {
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
Expand All @@ -195,7 +208,7 @@ export function ValueStorage({
};
};
},

// #MARK: setAttribute
setAttribute<
KEY extends keyof ATTRIBUTES,
VALUE extends ATTRIBUTES[KEY],
Expand All @@ -222,6 +235,7 @@ export function ValueStorage({
RunCallbacks(current);
},

// #MARK: setAttributes
setAttributes(newAttributes: ATTRIBUTES) {
if (is.equal(entity.attributes, newAttributes)) {
return;
Expand All @@ -234,6 +248,20 @@ export function ValueStorage({
RunCallbacks({ attributes: entity.attributes });
},

// #MARK: setConfiguration
setConfiguration(newConfiguration: CONFIGURATION) {
if (is.equal(entity.configuration, newConfiguration)) {
return;
}
entity.configuration = newConfiguration;
logger.trace(
{ id, name: registry.domain, newConfiguration },
`update configuration (all)`,
);
RunCallbacks({ configuration: entity.configuration });
},

// #MARK: setState
setState(newState: STATE) {
if (entity.state === newState) {
return;
Expand Down
Loading

0 comments on commit 024d431

Please sign in to comment.