@hashsome/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +18 -0
- package/dist/integration.d.ts +15 -1
- package/dist/local-client.d.ts +3 -1
- package/dist/local-client.js +22 -0
- package/dist/mock-forecast.d.ts +8 -0
- package/dist/mock-forecast.js +136 -0
- package/dist/mock-history.d.ts +9 -0
- package/dist/mock-history.js +65 -0
- package/dist/mock-logbook.d.ts +8 -0
- package/dist/mock-logbook.js +43 -0
- package/dist/mock.d.ts +7 -1
- package/dist/mock.js +37 -0
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.js +1 -0
- package/dist/model/logbook.d.ts +26 -0
- package/dist/model/logbook.js +1 -0
- package/dist/model/weather.d.ts +72 -0
- package/dist/model/weather.js +3 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +4 -1
- package/package.json +1 -1
- package/dist/base-integration.js.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/codegen.js.map +0 -1
- package/dist/conformance.js.map +0 -1
- package/dist/emitter.js.map +0 -1
- package/dist/entity.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integration.js.map +0 -1
- package/dist/local-client.js.map +0 -1
- package/dist/mock.js.map +0 -1
- package/dist/model/action.js.map +0 -1
- package/dist/model/base.js.map +0 -1
- package/dist/model/browse.js.map +0 -1
- package/dist/model/climate.js.map +0 -1
- package/dist/model/generic.js.map +0 -1
- package/dist/model/history.js.map +0 -1
- package/dist/model/index.js.map +0 -1
- package/dist/model/light.js.map +0 -1
- package/dist/model/media-player.js.map +0 -1
- package/dist/model/person.js.map +0 -1
- package/dist/model/sensor.js.map +0 -1
- package/dist/model/switch.js.map +0 -1
- package/dist/model/weather.js.map +0 -1
- package/dist/protocol.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -101,6 +101,24 @@ export class RemoteClient {
|
|
|
101
101
|
args: { ...query },
|
|
102
102
|
})));
|
|
103
103
|
}
|
|
104
|
+
async forecast(ref, query) {
|
|
105
|
+
return (await this.#request((id) => ({
|
|
106
|
+
type: 'query',
|
|
107
|
+
id,
|
|
108
|
+
ref,
|
|
109
|
+
query: 'forecast',
|
|
110
|
+
args: { ...query },
|
|
111
|
+
})));
|
|
112
|
+
}
|
|
113
|
+
async logbook(ref, query) {
|
|
114
|
+
return (await this.#request((id) => ({
|
|
115
|
+
type: 'query',
|
|
116
|
+
id,
|
|
117
|
+
ref,
|
|
118
|
+
query: 'logbook',
|
|
119
|
+
args: { ...query },
|
|
120
|
+
})));
|
|
121
|
+
}
|
|
104
122
|
callRaw(integration, request) {
|
|
105
123
|
return this.#request((id) => ({ type: 'raw', id, integration, request }));
|
|
106
124
|
}
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityRef } from './entity.ts';
|
|
2
|
-
import type { BrowseQuery, BrowseResult, Entity, HistoryQuery, HistoryResult } from './model/index.ts';
|
|
2
|
+
import type { BrowseQuery, BrowseResult, Entity, ForecastQuery, ForecastResult, HistoryQuery, HistoryResult, LogbookQuery, LogbookResult } from './model/index.ts';
|
|
3
3
|
export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
|
|
4
4
|
export type Unsubscribe = () => void;
|
|
5
5
|
/**
|
|
@@ -48,6 +48,20 @@ export interface Integration {
|
|
|
48
48
|
* Arguments come from a browser: validate them.
|
|
49
49
|
*/
|
|
50
50
|
history?(entityId: string, query: HistoryQuery): Promise<HistoryResult>;
|
|
51
|
+
/**
|
|
52
|
+
* A weather entity's forecast of the asked kind (days, hours, half-days), soonest first. Optional:
|
|
53
|
+
* only for backends that have forecasts. Resolves with no points when the entity cannot give that
|
|
54
|
+
* kind, and rejects with a displayable `Error` for an unknown entity. Arguments come from a
|
|
55
|
+
* browser: validate them.
|
|
56
|
+
*/
|
|
57
|
+
forecast?(entityId: string, query: ForecastQuery): Promise<ForecastResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Recent activity of an entity: what happened to it (turned on, went offline) and who or what
|
|
60
|
+
* caused it, newest first. Optional: only for backends that keep such a record (Home Assistant's
|
|
61
|
+
* logbook). Resolves with no entries for an entity with none, and rejects with a displayable
|
|
62
|
+
* `Error` for an unknown entity. Arguments come from a browser: validate them.
|
|
63
|
+
*/
|
|
64
|
+
logbook?(entityId: string, query: LogbookQuery): Promise<LogbookResult>;
|
|
51
65
|
/**
|
|
52
66
|
* Fetches a file the backend serves for its entities, such as artwork, with the integration's own
|
|
53
67
|
* credentials. `path` is the backend's own path and starts with `/`; it comes from a browser, so
|
package/dist/local-client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Client, EntityListener, LinkStatus } from './client.ts';
|
|
2
2
|
import { type EntityRef } from './entity.ts';
|
|
3
3
|
import type { ConnectionStatus, Integration, Unsubscribe } from './integration.ts';
|
|
4
|
-
import type { BrowseQuery, BrowseResult, Entity, HistoryQuery, HistoryResult } from './model/index.ts';
|
|
4
|
+
import type { BrowseQuery, BrowseResult, Entity, ForecastQuery, ForecastResult, HistoryQuery, HistoryResult, LogbookQuery, LogbookResult } from './model/index.ts';
|
|
5
5
|
/** In-process `Client` over integrations, with no runtime server. Used by the gallery and tests. */
|
|
6
6
|
export declare class LocalClient implements Client {
|
|
7
7
|
#private;
|
|
@@ -18,5 +18,7 @@ export declare class LocalClient implements Client {
|
|
|
18
18
|
command(ref: EntityRef, name: string, args?: Record<string, unknown>): Promise<void>;
|
|
19
19
|
browse(ref: EntityRef, query: BrowseQuery): Promise<BrowseResult>;
|
|
20
20
|
history(ref: EntityRef, query: HistoryQuery): Promise<HistoryResult>;
|
|
21
|
+
forecast(ref: EntityRef, query: ForecastQuery): Promise<ForecastResult>;
|
|
22
|
+
logbook(ref: EntityRef, query: LogbookQuery): Promise<LogbookResult>;
|
|
21
23
|
callRaw(integration: string, request: Record<string, unknown>): Promise<unknown>;
|
|
22
24
|
}
|
package/dist/local-client.js
CHANGED
|
@@ -99,6 +99,28 @@ export class LocalClient {
|
|
|
99
99
|
}
|
|
100
100
|
return source.history(id, query);
|
|
101
101
|
}
|
|
102
|
+
forecast(ref, query) {
|
|
103
|
+
const { integration, id } = parseEntityRef(ref);
|
|
104
|
+
const source = this.#integrations.get(integration);
|
|
105
|
+
if (!source) {
|
|
106
|
+
return Promise.reject(new Error(`Unknown integration "${integration}"`));
|
|
107
|
+
}
|
|
108
|
+
if (!source.forecast) {
|
|
109
|
+
return Promise.reject(new Error(`"${integration}" has no forecasts`));
|
|
110
|
+
}
|
|
111
|
+
return source.forecast(id, query);
|
|
112
|
+
}
|
|
113
|
+
logbook(ref, query) {
|
|
114
|
+
const { integration, id } = parseEntityRef(ref);
|
|
115
|
+
const source = this.#integrations.get(integration);
|
|
116
|
+
if (!source) {
|
|
117
|
+
return Promise.reject(new Error(`Unknown integration "${integration}"`));
|
|
118
|
+
}
|
|
119
|
+
if (!source.logbook) {
|
|
120
|
+
return Promise.reject(new Error(`"${integration}" keeps no activity`));
|
|
121
|
+
}
|
|
122
|
+
return source.logbook(id, query);
|
|
123
|
+
}
|
|
102
124
|
callRaw(integration, request) {
|
|
103
125
|
const source = this.#integrations.get(integration);
|
|
104
126
|
if (!source) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Entity, ForecastQuery, ForecastResult } from './model/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A made-up but believable forecast for a mock weather entity, so the UI that a real source feeds
|
|
4
|
+
* has something to show: a spell of weather moving through, warmer afternoons and colder nights
|
|
5
|
+
* around the entity's current temperature. The same every time for the same entity and moment (`now`
|
|
6
|
+
* is a parameter so a test can pin it). Anything that is not a weather entity has none.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mockForecast(entity: Entity | undefined, query: ForecastQuery, now?: number): ForecastResult;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const HOUR = 3_600_000;
|
|
2
|
+
const DAY = 24 * HOUR;
|
|
3
|
+
/** A spell of weather the made-up days run through, so a forecast is not one flat condition. */
|
|
4
|
+
const SPELL = [
|
|
5
|
+
'sunny',
|
|
6
|
+
'partlycloudy',
|
|
7
|
+
'cloudy',
|
|
8
|
+
'rainy',
|
|
9
|
+
'pouring',
|
|
10
|
+
'partlycloudy',
|
|
11
|
+
'sunny',
|
|
12
|
+
'sunny',
|
|
13
|
+
'cloudy',
|
|
14
|
+
'fog',
|
|
15
|
+
];
|
|
16
|
+
/** How likely rain is under each condition, percent. */
|
|
17
|
+
const RAIN = {
|
|
18
|
+
cloudy: 15,
|
|
19
|
+
fog: 10,
|
|
20
|
+
rainy: 70,
|
|
21
|
+
pouring: 95,
|
|
22
|
+
'lightning-rainy': 90,
|
|
23
|
+
'snowy-rainy': 80,
|
|
24
|
+
};
|
|
25
|
+
/** How much of the sky is cloud under each condition, percent. */
|
|
26
|
+
const CLOUD = {
|
|
27
|
+
sunny: 4,
|
|
28
|
+
'clear-night': 4,
|
|
29
|
+
partlycloudy: 50,
|
|
30
|
+
cloudy: 90,
|
|
31
|
+
fog: 100,
|
|
32
|
+
rainy: 95,
|
|
33
|
+
pouring: 100,
|
|
34
|
+
'lightning-rainy': 100,
|
|
35
|
+
};
|
|
36
|
+
const STEPS = { daily: 10, hourly: 48, twice_daily: 14 };
|
|
37
|
+
const round = (value) => Math.round(value * 10) / 10;
|
|
38
|
+
/**
|
|
39
|
+
* A made-up but believable forecast for a mock weather entity, so the UI that a real source feeds
|
|
40
|
+
* has something to show: a spell of weather moving through, warmer afternoons and colder nights
|
|
41
|
+
* around the entity's current temperature. The same every time for the same entity and moment (`now`
|
|
42
|
+
* is a parameter so a test can pin it). Anything that is not a weather entity has none.
|
|
43
|
+
*/
|
|
44
|
+
export function mockForecast(entity, query, now = Date.now()) {
|
|
45
|
+
if (entity?.kind !== 'weather') {
|
|
46
|
+
return { type: query.type, points: [] };
|
|
47
|
+
}
|
|
48
|
+
const base = entity.temperature ?? 15;
|
|
49
|
+
const start = new Date(now);
|
|
50
|
+
const points = [];
|
|
51
|
+
// The wind swings round slowly and gusts up with the rain.
|
|
52
|
+
const wind = (step) => entity.windSpeed === undefined
|
|
53
|
+
? {}
|
|
54
|
+
: {
|
|
55
|
+
windSpeed: round(entity.windSpeed * (0.7 + 0.6 * Math.abs(Math.sin(step * 0.7)))),
|
|
56
|
+
windBearing: Math.round(((entity.windBearing ?? 0) + step * 9) % 360),
|
|
57
|
+
};
|
|
58
|
+
const spellAt = (step) => SPELL[(step + Math.max(0, SPELL.indexOf(entity.condition))) % SPELL.length];
|
|
59
|
+
if (query.type === 'hourly') {
|
|
60
|
+
start.setMinutes(0, 0, 0);
|
|
61
|
+
for (let i = 0; i < STEPS.hourly; i += 1) {
|
|
62
|
+
const time = new Date(start.getTime() + i * HOUR);
|
|
63
|
+
const hours = time.getHours();
|
|
64
|
+
const condition = spellAt(Math.floor(i / 8));
|
|
65
|
+
const night = hours < 6 || hours >= 21;
|
|
66
|
+
points.push({
|
|
67
|
+
timestamp: time.toISOString(),
|
|
68
|
+
condition: night && condition === 'sunny' ? 'clear-night' : condition,
|
|
69
|
+
temperature: round(base + 4 * Math.sin(((hours - 9) / 24) * 2 * Math.PI) - i * 0.05),
|
|
70
|
+
precipitationProbability: RAIN[condition] ?? 0,
|
|
71
|
+
...wind(i),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (query.type === 'twice_daily') {
|
|
76
|
+
start.setHours(0, 0, 0, 0);
|
|
77
|
+
for (let i = 0; i < STEPS.twice_daily; i += 1) {
|
|
78
|
+
const daytime = i % 2 === 0;
|
|
79
|
+
const condition = spellAt(Math.floor(i / 2));
|
|
80
|
+
points.push({
|
|
81
|
+
timestamp: new Date(start.getTime() + i * 12 * HOUR + (daytime ? 6 * HOUR : 0)).toISOString(),
|
|
82
|
+
condition: !daytime && condition === 'sunny' ? 'clear-night' : condition,
|
|
83
|
+
temperature: round(daytime ? base + 4 + Math.sin(i) : base - 4 + Math.sin(i)),
|
|
84
|
+
precipitationProbability: RAIN[condition] ?? 0,
|
|
85
|
+
daytime,
|
|
86
|
+
...wind(i),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
start.setHours(0, 0, 0, 0);
|
|
92
|
+
for (let i = 0; i < STEPS.daily; i += 1) {
|
|
93
|
+
const condition = spellAt(i);
|
|
94
|
+
const drift = 2 * Math.sin(i * 1.3);
|
|
95
|
+
points.push({
|
|
96
|
+
timestamp: new Date(start.getTime() + i * DAY).toISOString(),
|
|
97
|
+
condition,
|
|
98
|
+
temperature: round(base + 3 + drift),
|
|
99
|
+
low: round(base - 5 + drift),
|
|
100
|
+
precipitationProbability: RAIN[condition] ?? 0,
|
|
101
|
+
...(RAIN[condition] ? { precipitation: round((RAIN[condition] ?? 0) / 12) } : {}),
|
|
102
|
+
...wind(i),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// What else a weather source gives for a step, made up from the step itself: it is more humid when
|
|
107
|
+
// cold and cloudy, the sun peaks at noon (or at its day's best, for a whole day) and is dimmed by cloud,
|
|
108
|
+
// and gusts and the feels-like follow the wind.
|
|
109
|
+
const enrich = (point) => {
|
|
110
|
+
const cloud = CLOUD[point.condition] ?? 50;
|
|
111
|
+
const temperature = point.temperature ?? base;
|
|
112
|
+
const hour = new Date(point.timestamp).getHours();
|
|
113
|
+
const sun = query.type === 'daily' ? 1 : Math.max(0, Math.sin(((hour - 6) / 12) * Math.PI));
|
|
114
|
+
const rain = RAIN[point.condition] ?? 0;
|
|
115
|
+
return {
|
|
116
|
+
...point,
|
|
117
|
+
humidity: Math.min(100, Math.max(20, Math.round(85 - (temperature - 5) * 2 + cloud * 0.1))),
|
|
118
|
+
cloudCoverage: cloud,
|
|
119
|
+
uvIndex: point.daytime === false ? 0 : round(sun * 8 * (1 - cloud / 140)),
|
|
120
|
+
apparentTemperature: round(temperature - 1 - (point.windSpeed ?? 0) * 0.05),
|
|
121
|
+
pressure: Math.round(1015 - rain * 0.12 + Math.sin(hour / 4)),
|
|
122
|
+
...(point.windSpeed !== undefined ? { windGustSpeed: round(point.windSpeed * 1.6) } : {}),
|
|
123
|
+
...(rain > 50 && point.precipitation === undefined
|
|
124
|
+
? { precipitation: round(rain / 70) }
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
type: query.type,
|
|
130
|
+
points: points.map(enrich),
|
|
131
|
+
...(entity.unit ? { unit: entity.unit } : {}),
|
|
132
|
+
...(entity.precipitationUnit ? { precipitationUnit: entity.precipitationUnit } : {}),
|
|
133
|
+
...(entity.pressureUnit ? { pressureUnit: entity.pressureUnit } : {}),
|
|
134
|
+
...(entity.windUnit ? { windUnit: entity.windUnit } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Entity, HistoryQuery, HistoryResult } from './model/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Made-up but believable history for a numeric mock sensor, so the charts a real backend would fill
|
|
4
|
+
* have something to show: a daily rhythm (more in the evening), a slower drift over the days and a
|
|
5
|
+
* little jitter. A reading (a temperature, power) hovers around the sensor's current value; an
|
|
6
|
+
* `energy` counter (kWh or Wh) answers with how much it grew in each bucket. Nothing else has any.
|
|
7
|
+
* `now` is a parameter so a test can pin it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mockHistory(entityId: string, entity: Entity | undefined, query: HistoryQuery, now?: number): HistoryResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const MINUTE = 60_000;
|
|
2
|
+
const HOUR = 60 * MINUTE;
|
|
3
|
+
const DAY = 24 * HOUR;
|
|
4
|
+
const SPAN = { '1h': HOUR, '1d': DAY, '1w': 7 * DAY, '1m': 30 * DAY };
|
|
5
|
+
const BUCKET = { '5m': 5 * MINUTE, '1h': HOUR, '1d': DAY };
|
|
6
|
+
const DEFAULT_BUCKET = {
|
|
7
|
+
'1h': '5m',
|
|
8
|
+
'1d': '5m',
|
|
9
|
+
'1w': '1h',
|
|
10
|
+
'1m': '1h',
|
|
11
|
+
};
|
|
12
|
+
/** A number in [0, 1) that is the same for the same text, so a mock device always has the same past. */
|
|
13
|
+
function seed(text) {
|
|
14
|
+
let hash = 0;
|
|
15
|
+
for (const char of text) {
|
|
16
|
+
hash = (hash * 31 + char.codePointAt(0)) % 100_003;
|
|
17
|
+
}
|
|
18
|
+
return hash / 100_003;
|
|
19
|
+
}
|
|
20
|
+
const round = (value, digits) => {
|
|
21
|
+
const factor = 10 ** digits;
|
|
22
|
+
return Math.round(value * factor) / factor;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Made-up but believable history for a numeric mock sensor, so the charts a real backend would fill
|
|
26
|
+
* have something to show: a daily rhythm (more in the evening), a slower drift over the days and a
|
|
27
|
+
* little jitter. A reading (a temperature, power) hovers around the sensor's current value; an
|
|
28
|
+
* `energy` counter (kWh or Wh) answers with how much it grew in each bucket. Nothing else has any.
|
|
29
|
+
* `now` is a parameter so a test can pin it.
|
|
30
|
+
*/
|
|
31
|
+
export function mockHistory(entityId, entity, query, now = Date.now()) {
|
|
32
|
+
if (entity?.kind !== 'sensor' || entity.numeric === undefined) {
|
|
33
|
+
return { points: [], kind: 'measurement' };
|
|
34
|
+
}
|
|
35
|
+
const total = entity.measurement === 'energy' || entity.unit === 'kWh' || entity.unit === 'Wh';
|
|
36
|
+
const bucket = BUCKET[query.bucket ?? DEFAULT_BUCKET[query.range]];
|
|
37
|
+
const count = Math.ceil(SPAN[query.range] / bucket);
|
|
38
|
+
const end = Math.floor(now / bucket) * bucket;
|
|
39
|
+
const phase = seed(entityId);
|
|
40
|
+
// How busy the device is at a time: 0 to 1.
|
|
41
|
+
const level = (time) => {
|
|
42
|
+
const hours = (time % DAY) / HOUR;
|
|
43
|
+
const daily = 0.5 + 0.5 * Math.sin(((hours - 9) / 24) * 2 * Math.PI);
|
|
44
|
+
const drift = 0.15 * Math.sin((time / DAY) * 1.3 + phase * 6);
|
|
45
|
+
const jitter = 0.015 * Math.sin((time / bucket) * 1.7 + phase * 9);
|
|
46
|
+
return Math.min(1, Math.max(0, daily + drift + jitter));
|
|
47
|
+
};
|
|
48
|
+
// What a counter grows by in a day: about 0.6 to 1.8 kWh, and the same in Wh.
|
|
49
|
+
const perDay = (0.6 + 1.2 * phase) * (entity.unit === 'Wh' ? 1000 : 1);
|
|
50
|
+
const points = [];
|
|
51
|
+
for (let i = count - 1; i >= 0; i -= 1) {
|
|
52
|
+
const start = end - i * bucket;
|
|
53
|
+
points.push({
|
|
54
|
+
timestamp: new Date(start).toISOString(),
|
|
55
|
+
value: total
|
|
56
|
+
? round((perDay / 24) * (bucket / HOUR) * (0.4 + 1.2 * level(start)), 3)
|
|
57
|
+
: round(entity.numeric * (0.55 + 0.9 * level(start)), 1),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
points,
|
|
62
|
+
kind: total ? 'total' : 'measurement',
|
|
63
|
+
...(entity.unit ? { unit: entity.unit } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Entity, LogbookQuery, LogbookResult } from './model/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Made-up but believable activity for a mock light, switch or heater, so the History section of its
|
|
4
|
+
* drawer has something to show: it turns on and off through the past days, caused by a person or an
|
|
5
|
+
* automation, now and then going offline by itself. The same every time for the same entity and
|
|
6
|
+
* moment (`now` is a parameter so a test can pin it). Anything else has none.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mockLogbook(entityId: string, entity: Entity | undefined, query: LogbookQuery, now?: number): LogbookResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const MINUTE = 60_000;
|
|
2
|
+
const HOUR = 60 * MINUTE;
|
|
3
|
+
/** Who the made-up activity is caused by: two people at home, and two automations. */
|
|
4
|
+
const ACTORS = [
|
|
5
|
+
{ actor: 'Dan' },
|
|
6
|
+
{ actor: 'Bedtime', actorKind: 'automation' },
|
|
7
|
+
{ actor: 'Alex' },
|
|
8
|
+
{ actor: 'Sunrise', actorKind: 'automation' },
|
|
9
|
+
{},
|
|
10
|
+
];
|
|
11
|
+
/** The kinds of entity that switch on and off, and so have activity worth showing. */
|
|
12
|
+
const SWITCHING = new Set(['light', 'switch', 'climate']);
|
|
13
|
+
/**
|
|
14
|
+
* Made-up but believable activity for a mock light, switch or heater, so the History section of its
|
|
15
|
+
* drawer has something to show: it turns on and off through the past days, caused by a person or an
|
|
16
|
+
* automation, now and then going offline by itself. The same every time for the same entity and
|
|
17
|
+
* moment (`now` is a parameter so a test can pin it). Anything else has none.
|
|
18
|
+
*/
|
|
19
|
+
export function mockLogbook(entityId, entity, query, now = Date.now()) {
|
|
20
|
+
if (!entity || !SWITCHING.has(entity.kind)) {
|
|
21
|
+
return { entries: [] };
|
|
22
|
+
}
|
|
23
|
+
const limit = Math.max(1, Math.min(query.limit ?? 20, 100));
|
|
24
|
+
const entries = [];
|
|
25
|
+
let on = entity.kind === 'light' ? entity.on === true : true;
|
|
26
|
+
for (let i = 0; i < limit; i += 1) {
|
|
27
|
+
// Further back and a little irregular, newest first.
|
|
28
|
+
const at = now - (i + 1) * 3 * HOUR - ((i * 7919) % 50) * MINUTE;
|
|
29
|
+
const cause = ACTORS[(i + entityId.length) % ACTORS.length];
|
|
30
|
+
const lost = i % 9 === 8;
|
|
31
|
+
entries.push({
|
|
32
|
+
id: `${entityId}@${at}`,
|
|
33
|
+
message: lost ? 'became unavailable' : on ? 'turned on' : 'turned off',
|
|
34
|
+
timestamp: new Date(at).toISOString(),
|
|
35
|
+
change: lost ? 'availability' : on ? 'on' : 'off',
|
|
36
|
+
...(lost ? {} : cause),
|
|
37
|
+
});
|
|
38
|
+
if (!lost) {
|
|
39
|
+
on = !on;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { entries };
|
|
43
|
+
}
|
package/dist/mock.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseIntegration } from './base-integration.ts';
|
|
2
|
-
import { type ActionEntity, type BrowseItem, type BrowseQuery, type BrowseResult, type ClimateEntity, type EntityInput, type GenericEntity, type LightEntity, type MediaPlayerEntity, type PersonEntity, type SensorEntity, type SwitchEntity, type WeatherEntity } from './model/index.ts';
|
|
2
|
+
import { type ActionEntity, type BrowseItem, type BrowseQuery, type BrowseResult, type ClimateEntity, type EntityInput, type ForecastQuery, type ForecastResult, type GenericEntity, type HistoryQuery, type HistoryResult, type LogbookQuery, type LogbookResult, type LightEntity, type MediaPlayerEntity, type PersonEntity, type SensorEntity, type SwitchEntity, type WeatherEntity } from './model/index.ts';
|
|
3
3
|
type Init<E extends {
|
|
4
4
|
kind: string;
|
|
5
5
|
ref: unknown;
|
|
@@ -55,6 +55,12 @@ export declare class MockIntegration extends BaseIntegration {
|
|
|
55
55
|
set(entityId: string, input: EntityInput): void;
|
|
56
56
|
/** Patches an entity: top-level fields are replaced, `capabilities` and `media` are merged. */
|
|
57
57
|
update(entityId: string, patch: Partial<EntityInput>): void;
|
|
58
|
+
/** Made-up history for a numeric sensor (see `mockHistory`); empty for anything else. */
|
|
59
|
+
history(entityId: string, query: HistoryQuery): Promise<HistoryResult>;
|
|
60
|
+
/** Made-up activity for a light, switch or heater (see `mockLogbook`); empty for anything else. */
|
|
61
|
+
logbook(entityId: string, query: LogbookQuery): Promise<LogbookResult>;
|
|
62
|
+
/** Made-up forecast for a weather entity (see `mockForecast`); empty for anything else. */
|
|
63
|
+
forecast(entityId: string, query: ForecastQuery): Promise<ForecastResult>;
|
|
58
64
|
browse(entityId: string, query: BrowseQuery): Promise<BrowseResult>;
|
|
59
65
|
command(entityId: string, name: string, args?: Record<string, unknown>): Promise<void>;
|
|
60
66
|
}
|
package/dist/mock.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { BaseIntegration } from './base-integration.js';
|
|
2
2
|
import { UnknownEntityError } from './entity.js';
|
|
3
|
+
import { mockForecast } from './mock-forecast.js';
|
|
4
|
+
import { mockHistory } from './mock-history.js';
|
|
5
|
+
import { mockLogbook } from './mock-logbook.js';
|
|
3
6
|
import { isCommandOf, } from './model/index.js';
|
|
4
7
|
export function mockMediaPlayer(init = {}) {
|
|
5
8
|
const { capabilities, ...rest } = init;
|
|
@@ -95,6 +98,16 @@ export function mockWeather(init = {}) {
|
|
|
95
98
|
condition: 'sunny',
|
|
96
99
|
temperature: 20,
|
|
97
100
|
unit: '°C',
|
|
101
|
+
windSpeed: 14,
|
|
102
|
+
windBearing: 225,
|
|
103
|
+
windUnit: 'km/h',
|
|
104
|
+
uvIndex: 3,
|
|
105
|
+
cloudCoverage: 40,
|
|
106
|
+
apparentTemperature: 18,
|
|
107
|
+
pressure: 1015,
|
|
108
|
+
pressureUnit: 'hPa',
|
|
109
|
+
precipitationUnit: 'mm',
|
|
110
|
+
forecasts: ['daily', 'hourly'],
|
|
98
111
|
...init,
|
|
99
112
|
};
|
|
100
113
|
}
|
|
@@ -377,6 +390,30 @@ export class MockIntegration extends BaseIntegration {
|
|
|
377
390
|
}
|
|
378
391
|
this.set(entityId, merged);
|
|
379
392
|
}
|
|
393
|
+
/** Made-up history for a numeric sensor (see `mockHistory`); empty for anything else. */
|
|
394
|
+
history(entityId, query) {
|
|
395
|
+
const entity = this.getEntity(entityId);
|
|
396
|
+
if (!entity) {
|
|
397
|
+
return Promise.reject(new UnknownEntityError(this.id, entityId));
|
|
398
|
+
}
|
|
399
|
+
return Promise.resolve(mockHistory(entityId, entity, query));
|
|
400
|
+
}
|
|
401
|
+
/** Made-up activity for a light, switch or heater (see `mockLogbook`); empty for anything else. */
|
|
402
|
+
logbook(entityId, query) {
|
|
403
|
+
const entity = this.getEntity(entityId);
|
|
404
|
+
if (!entity) {
|
|
405
|
+
return Promise.reject(new UnknownEntityError(this.id, entityId));
|
|
406
|
+
}
|
|
407
|
+
return Promise.resolve(mockLogbook(entityId, entity, query));
|
|
408
|
+
}
|
|
409
|
+
/** Made-up forecast for a weather entity (see `mockForecast`); empty for anything else. */
|
|
410
|
+
forecast(entityId, query) {
|
|
411
|
+
const entity = this.getEntity(entityId);
|
|
412
|
+
if (!entity) {
|
|
413
|
+
return Promise.reject(new UnknownEntityError(this.id, entityId));
|
|
414
|
+
}
|
|
415
|
+
return Promise.resolve(mockForecast(entity, query));
|
|
416
|
+
}
|
|
380
417
|
browse(entityId, query) {
|
|
381
418
|
if (!this.getEntity(entityId)) {
|
|
382
419
|
return Promise.reject(new UnknownEntityError(this.id, entityId));
|
package/dist/model/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { WeatherCommands, WeatherEntity } from './weather.ts';
|
|
|
10
10
|
export * from './base.ts';
|
|
11
11
|
export * from './browse.ts';
|
|
12
12
|
export * from './history.ts';
|
|
13
|
+
export * from './logbook.ts';
|
|
13
14
|
export * from './action.ts';
|
|
14
15
|
export * from './climate.ts';
|
|
15
16
|
export * from './generic.ts';
|
package/dist/model/index.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** One thing that happened to an entity: it turned on, went offline, ... and what or who caused it. */
|
|
2
|
+
export interface LogbookEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Lowercase, no actor prefix, e.g. "turned on", "became unavailable": capitalized when there is
|
|
5
|
+
* no `actor` to lead with ("Turned on"), left lowercase after one ("Dan turned on"). */
|
|
6
|
+
message: string;
|
|
7
|
+
/** ISO 8601. */
|
|
8
|
+
timestamp: string;
|
|
9
|
+
/** Who or what caused it, e.g. "Dan" or "Bedtime". Omitted when it was the system (a device going offline). */
|
|
10
|
+
actor?: string;
|
|
11
|
+
/** Only matters when `actor` is set: a person gets an initials avatar, an automation (or anything
|
|
12
|
+
* else that is not a person) a workflow icon. Default `'person'`. */
|
|
13
|
+
actorKind?: 'person' | 'automation';
|
|
14
|
+
/** What kind of change it was: it turned `on` or `off`, took another `state` (a new mode), or its
|
|
15
|
+
* `availability` changed (the device could not be reached, or came back). Lets a row say more than
|
|
16
|
+
* the words when nothing caused it: an on or off with no actor is often someone at a physical switch. */
|
|
17
|
+
change?: 'on' | 'off' | 'state' | 'availability';
|
|
18
|
+
}
|
|
19
|
+
export interface LogbookQuery {
|
|
20
|
+
/** How many of the latest entries to return. Default 20. */
|
|
21
|
+
limit?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface LogbookResult {
|
|
24
|
+
/** Newest first; empty when the backend keeps no activity for the entity. */
|
|
25
|
+
entries: LogbookEntry[];
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/model/weather.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ import type { EntityBase } from './base.ts';
|
|
|
3
3
|
* reports that is not one of these is `unknown`. */
|
|
4
4
|
export declare const WEATHER_CONDITIONS: readonly ['sunny', 'clear-night', 'partlycloudy', 'cloudy', 'fog', 'rainy', 'pouring', 'snowy', 'snowy-rainy', 'hail', 'lightning', 'lightning-rainy', 'windy', 'exceptional', 'unknown'];
|
|
5
5
|
export type WeatherCondition = (typeof WEATHER_CONDITIONS)[number];
|
|
6
|
+
/** What a forecast looks ahead in: `daily` is a day per point, `hourly` an hour, and `twice_daily` a
|
|
7
|
+
* day and a night. A weather source supports some of them. */
|
|
8
|
+
export declare const FORECAST_TYPES: readonly ['daily', 'hourly', 'twice_daily'];
|
|
9
|
+
export type ForecastType = (typeof FORECAST_TYPES)[number];
|
|
6
10
|
/** Current weather for a place. */
|
|
7
11
|
export interface WeatherEntity extends EntityBase<'weather'> {
|
|
8
12
|
condition: WeatherCondition;
|
|
@@ -12,5 +16,73 @@ export interface WeatherEntity extends EntityBase<'weather'> {
|
|
|
12
16
|
unit?: string;
|
|
13
17
|
/** Percent. */
|
|
14
18
|
humidity?: number;
|
|
19
|
+
/** How hard the wind blows, in `windUnit`. */
|
|
20
|
+
windSpeed?: number;
|
|
21
|
+
/** Where the wind comes from, in degrees: 0 is north, 90 east. */
|
|
22
|
+
windBearing?: number;
|
|
23
|
+
/** `km/h`, `mph`, `m/s`, ... */
|
|
24
|
+
windUnit?: string;
|
|
25
|
+
/** UV index: 0 to 11 and more. */
|
|
26
|
+
uvIndex?: number;
|
|
27
|
+
/** Percent of the sky covered by cloud. */
|
|
28
|
+
cloudCoverage?: number;
|
|
29
|
+
/** What it feels like, in `unit`: the temperature with the wind and humidity counted in. */
|
|
30
|
+
apparentTemperature?: number;
|
|
31
|
+
/** Air pressure, in `pressureUnit`. */
|
|
32
|
+
pressure?: number;
|
|
33
|
+
/** `hPa`, `inHg`, ... */
|
|
34
|
+
pressureUnit?: string;
|
|
35
|
+
/** `mm` or `in`: the unit of the rain a forecast gives. */
|
|
36
|
+
precipitationUnit?: string;
|
|
37
|
+
/** The forecasts the weather source can give, if any. Ask for one with `Client.forecast`. */
|
|
38
|
+
forecasts?: ForecastType[];
|
|
39
|
+
}
|
|
40
|
+
export interface ForecastQuery {
|
|
41
|
+
type: ForecastType;
|
|
42
|
+
}
|
|
43
|
+
/** One step of a forecast: a day, an hour, or half a day. */
|
|
44
|
+
export interface ForecastPoint {
|
|
45
|
+
/** When the step starts, ISO 8601. */
|
|
46
|
+
timestamp: string;
|
|
47
|
+
condition: WeatherCondition;
|
|
48
|
+
/** The temperature, or the day's high; in the result's `unit`. */
|
|
49
|
+
temperature?: number;
|
|
50
|
+
/** The day's low, for a forecast that covers a whole day. */
|
|
51
|
+
low?: number;
|
|
52
|
+
/** Percent chance of rain or snow. */
|
|
53
|
+
precipitationProbability?: number;
|
|
54
|
+
/** Expected rain or snow, in the result's `precipitationUnit`. */
|
|
55
|
+
precipitation?: number;
|
|
56
|
+
/** In the result's `windUnit`. */
|
|
57
|
+
windSpeed?: number;
|
|
58
|
+
/** Where the wind comes from, in degrees: 0 is north, 90 east. */
|
|
59
|
+
windBearing?: number;
|
|
60
|
+
/** The strongest gusts, in the result's `windUnit`. */
|
|
61
|
+
windGustSpeed?: number;
|
|
62
|
+
/** Percent. */
|
|
63
|
+
humidity?: number;
|
|
64
|
+
/** Percent of the sky covered by cloud. */
|
|
65
|
+
cloudCoverage?: number;
|
|
66
|
+
/** UV index. */
|
|
67
|
+
uvIndex?: number;
|
|
68
|
+
/** What it feels like, in the result's `unit`. */
|
|
69
|
+
apparentTemperature?: number;
|
|
70
|
+
/** Air pressure, in the result's `pressureUnit`. */
|
|
71
|
+
pressure?: number;
|
|
72
|
+
/** For `twice_daily`: whether this step is the day (true) or the night. */
|
|
73
|
+
daytime?: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface ForecastResult {
|
|
76
|
+
type: ForecastType;
|
|
77
|
+
/** Soonest first; empty when the source has no forecast of that type. */
|
|
78
|
+
points: ForecastPoint[];
|
|
79
|
+
/** `°C` or `°F`. */
|
|
80
|
+
unit?: string;
|
|
81
|
+
/** The unit of `windSpeed`: `km/h`, `mph`, ... */
|
|
82
|
+
windUnit?: string;
|
|
83
|
+
/** The unit of `precipitation`: `mm` or `in`. */
|
|
84
|
+
precipitationUnit?: string;
|
|
85
|
+
/** The unit of `pressure`: `hPa`, `inHg`, ... */
|
|
86
|
+
pressureUnit?: string;
|
|
15
87
|
}
|
|
16
88
|
export type WeatherCommands = Record<string, never>;
|
package/dist/model/weather.js
CHANGED
|
@@ -17,3 +17,6 @@ export const WEATHER_CONDITIONS = [
|
|
|
17
17
|
'exceptional',
|
|
18
18
|
'unknown',
|
|
19
19
|
];
|
|
20
|
+
/** What a forecast looks ahead in: `daily` is a day per point, `hourly` an hour, and `twice_daily` a
|
|
21
|
+
* day and a night. A weather source supports some of them. */
|
|
22
|
+
export const FORECAST_TYPES = ['daily', 'hourly', 'twice_daily'];
|
package/dist/protocol.d.ts
CHANGED
|
@@ -22,11 +22,11 @@ export type ClientMessage = {
|
|
|
22
22
|
command: string;
|
|
23
23
|
args?: Record<string, unknown>;
|
|
24
24
|
} | {
|
|
25
|
-
/** A read that is not a subscription: one level of a media library,
|
|
25
|
+
/** A read that is not a subscription: one level of a media library, an entity's history, a weather forecast, or its recent activity, answered by `result`. */
|
|
26
26
|
type: 'query';
|
|
27
27
|
id: number;
|
|
28
28
|
ref: EntityRef;
|
|
29
|
-
query: 'browse' | 'history';
|
|
29
|
+
query: 'browse' | 'history' | 'forecast' | 'logbook';
|
|
30
30
|
args?: Record<string, unknown>;
|
|
31
31
|
} | {
|
|
32
32
|
type: 'raw';
|
package/dist/protocol.js
CHANGED
|
@@ -42,7 +42,10 @@ export function parseClientMessage(raw) {
|
|
|
42
42
|
case 'query': {
|
|
43
43
|
if (typeof value.id !== 'number' ||
|
|
44
44
|
!isRef(value.ref) ||
|
|
45
|
-
(value.query !== 'browse' &&
|
|
45
|
+
(value.query !== 'browse' &&
|
|
46
|
+
value.query !== 'history' &&
|
|
47
|
+
value.query !== 'forecast' &&
|
|
48
|
+
value.query !== 'logbook') ||
|
|
46
49
|
(value.args !== undefined && !isRecord(value.args))) {
|
|
47
50
|
return undefined;
|
|
48
51
|
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"base-integration.js","sourceRoot":"","sources":["../src/base-integration.ts"],"names":[],"mappings":"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"}
|