Skip to content

Commit

Permalink
feat(calendar): Implement Calendar Event Trigger Node
Browse files Browse the repository at this point in the history
* Stub out basic calendar output node

* WIP Update UI with draft fields from last attempt

* Stuff it, these are the fields I will use

* WIP implement event to queue calendar events

* WIP implement calendar item timer queue

* WIP queue item events

* WIP trigger calendar event messages

* Add some todos

* WIP Refactor EventsCalendarController class

* linter fixes

* html delint

* Allow for mild overdue queuing

* Include a check for filter text

* Bug-fix: use offset when querying the API

* Implement custom outputs

* bit of an improvement to status messages

* Localise status messages

* Bug-fix: prevent queue time drift by basing next queue time on the end of the interval

* Include Expose As for the Events Calendar editor

* Remove Unnecessary constraint interface definition

* Refactor CalendarItem file structure

* Remove output count

---------

Co-authored-by: Nathan Brittain <git@enjibby.dev>
  • Loading branch information
enjibby and Nathan Brittain committed Feb 5, 2024
1 parent 0231584 commit f5a183c
Show file tree
Hide file tree
Showing 21 changed files with 706 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export default defineUserConfig({
'current-state',
'device',
'events-all',
'events-calendar',
'events-state',
'fire-event',
'get-entities',
Expand Down
46 changes: 46 additions & 0 deletions docs/node/events-calendar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Events: calendar

Outputs calendar item events similar to the calendar automation in Home Assistant

## Configuration:

### Entity ID <Badge text="required"/>

- Type: `string`

The entity_id for the calendar that contains triggerable calendar items.

### Relative To <Badge text="required"/>

- Type: `start | end`
- Default: `start`

Whether to trigger an event at the start or end of each matching calendar item.

### Offset <Badge text="required"/>

- Type: `number`
- Default: 0 seconds

A negative or positive amount of time to allow the event to be triggered before or after the calendar item's start/end time.

### Conditions

This node has two default outputs "allowed" and "blocked". If all the
conditions are true the calendar item will be sent to the output.

**See Also:**

- [Conditionals](/guide/conditionals.md)

### Expose to Home Assistant

- Type: `boolean`

Creates a switch within Home Assistant to enable/disable this node. This feature requires [Node-RED custom integration](https://github.com/zachowj/hass-node-red) to be installed in Home Assistant

## Outputs

Value types:

- `calendar item`: the calendar item object as provided by the Home Assistant API
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const nodeMap = {
entity: { doc: 'entity', type: 'ha-entity' },
'entity-config': { doc: 'entity-config', type: 'ha-entity-config' },
'events-all': { doc: 'events-all', type: 'server-events' },
'events-calendar': { doc: 'events-calendar', type: 'ha-events-calendar' },
'events-state': {
doc: 'events-state',
type: 'server-state-changed',
Expand Down
3 changes: 3 additions & 0 deletions icons/ha-events-calendar.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/common/services/TypedInputService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default class TypedInputService {
case TypedInputTypes.Results:
case TypedInputTypes.TriggerId:
case TypedInputTypes.Value:
case TypedInputTypes.CalendarItem:
val = props[valueType];
break;
default:
Expand Down
2 changes: 2 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export enum NodeType {
Device = 'ha-device',
Entity = 'ha-entity',
EventsAll = 'server-events',
EventsCalendar = 'ha-events-calendar',
EventsState = 'server-state-changed',
FireEvent = 'ha-fire-event',
GetEntities = 'ha-get-entities',
Expand Down Expand Up @@ -133,6 +134,7 @@ export enum TypedInputTypes {
Regex = 're',
Value = 'value',
PreviousValue = 'previousValue',
CalendarItem = 'calendarItem',
}

export enum TimeUnit {
Expand Down
2 changes: 2 additions & 0 deletions src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import DeviceConfigEditor from './nodes/device-config/editor';
import EntityEditor from './nodes/entity/editor';
import EntityConfigEditor from './nodes/entity-config/editor/editor';
import EventsAllEditor from './nodes/events-all/editor';
import EventsCalendarEditor from './nodes/events-calendar/editor';
import EventsStateEditor from './nodes/events-state/editor';
import FireEventEditor from './nodes/fire-event/editor';
import GetEntitiesEditor from './nodes/get-entities/editor';
Expand Down Expand Up @@ -80,6 +81,7 @@ RED.nodes.registerType(NodeType.CurrentState, CurrentStateEditor);
RED.nodes.registerType(NodeType.Device, DeviceEditor);
RED.nodes.registerType(NodeType.Entity, EntityEditor);
RED.nodes.registerType(NodeType.EventsAll, EventsAllEditor);
RED.nodes.registerType(NodeType.EventsCalendar, EventsCalendarEditor);
RED.nodes.registerType(NodeType.EventsState, EventsStateEditor);
RED.nodes.registerType(NodeType.FireEvent, FireEventEditor);
RED.nodes.registerType(NodeType.GetEntities, GetEntitiesEditor);
Expand Down
5 changes: 5 additions & 0 deletions src/editor/components/output-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const customTypes: { [key: string]: EditorWidgetTypedInputTypeDefinition } = {
},
triggerId: { value: 'triggerId', label: 'trigger id', hasValue: false },
value: { value: 'value', label: 'value', hasValue: false },
calendarItem: {
value: 'calendarItem',
label: 'calendar item',
hasValue: false,
},
};
const defaultTypes: HATypedInputTypeOptions = [
customTypes.config,
Expand Down
21 changes: 20 additions & 1 deletion src/editor/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ export function getAutocomplete(serverId: string, type: any) {
return list;
}

export type AutocompleteType = 'entities' | 'properties' | 'trackers' | 'zones';
export type AutocompleteType =
| 'entities'
| 'properties'
| 'trackers'
| 'zones'
| 'calendars';
export function getAutocompleteData(serverId: string, type: AutocompleteType) {
let list: { value: any; label: any }[] = [];
switch (type) {
Expand Down Expand Up @@ -127,6 +132,20 @@ export function getAutocompleteData(serverId: string, type: AutocompleteType) {
.sort(sortFriendlyName);
break;
}
case 'calendars': {
if (!(serverId in entities)) return [];
const path = 'attributes.friendly_name';
list = Object.values(entities[serverId])
.filter((item) => item.entity_id.startsWith('calendar.'))
.map((item) => {
return {
value: item.entity_id,
label: deepFind(path, item) || item.entity_id,
};
})
.sort(sortFriendlyName);
break;
}
}

return list;
Expand Down
2 changes: 2 additions & 0 deletions src/editor/exposenode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function init(n: HassNodeProperties) {
}
break;
case NodeType.EventsAll:
case NodeType.EventsCalendar:
case NodeType.EventsState:
case NodeType.PollState:
case NodeType.Tag:
Expand Down Expand Up @@ -131,6 +132,7 @@ function render() {
}
break;
case NodeType.EventsAll:
case NodeType.EventsCalendar:
case NodeType.EventsState:
case NodeType.PollState:
case NodeType.Tag:
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import deviceConfig from '../nodes/device-config/migrations';
import entity from '../nodes/entity/migrations';
import entityConfig from '../nodes/entity-config/migrations';
import eventsAll from '../nodes/events-all/migrations';
import eventsCalendar from '../nodes/events-calendar/migrations';
import eventsState from '../nodes/events-state/migrations';
import fireEvent from '../nodes/fire-event/migrations';
import getEntities from '../nodes/get-entities/migrations';
Expand Down Expand Up @@ -44,6 +45,7 @@ const nodeTypeTranslation: Partial<Record<NodeType, Migration[]>> = {
[NodeType.Entity]: entity,
[NodeType.EntityConfig]: entityConfig,
[NodeType.EventsAll]: eventsAll,
[NodeType.EventsCalendar]: eventsCalendar,
[NodeType.EventsState]: eventsState,
[NodeType.FireEvent]: fireEvent,
[NodeType.GetEntities]: getEntities,
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import deviceConfigNode from './nodes/device-config';
import entityNode from './nodes/entity';
import entityConfigNode from './nodes/entity-config';
import eventsAllNode from './nodes/events-all';
import eventsCalendarNode from './nodes/events-calendar';
import eventsStateNode from './nodes/events-state';
import fireEventNode from './nodes/fire-event';
import getEntitiesNode from './nodes/get-entities';
Expand Down Expand Up @@ -44,6 +45,7 @@ const nodes: Record<NodeType, any> = {
[NodeType.Device]: deviceNode,
[NodeType.Entity]: entityNode,
[NodeType.EventsAll]: eventsAllNode,
[NodeType.EventsCalendar]: eventsCalendarNode,
[NodeType.EventsState]: eventsStateNode,
[NodeType.FireEvent]: fireEventNode,
[NodeType.GetEntities]: getEntitiesNode,
Expand Down
41 changes: 41 additions & 0 deletions src/nodes/events-calendar/CalendarItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DateOrDateTime, toDate } from './helpers';

export interface ICalendarItem {
start: DateOrDateTime;
end: DateOrDateTime;
summary: string;
description: string;
location?: string | null;
uid: string;
recurrence_id?: string | null;
rrule?: string | null;
queueIndex(): string;
date(eventType: string): Date;
}

export class CalendarItem implements ICalendarItem {
start!: DateOrDateTime;
end!: DateOrDateTime;
summary!: string;
description!: string;
location?: string | null;
uid!: string;
recurrence_id?: string | null;
rrule?: string | null;

constructor(data: ICalendarItem) {
Object.assign(this, data);
}

queueIndex() {
return `${this.uid}${this.recurrence_id || ''}`;
}

date(eventType: string): Date {
return eventType === 'start' ? toDate(this.start) : toDate(this.end);
}
}

export function createCalendarItem(data: ICalendarItem): CalendarItem {
return new CalendarItem(data);
}

0 comments on commit f5a183c

Please sign in to comment.