Skip to content

Commit

Permalink
feat(number): Add "Expose as" option for Listening mode in number node
Browse files Browse the repository at this point in the history
  • Loading branch information
zachowj committed Sep 9, 2024
1 parent b4d15d9 commit e7441e1
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 60 deletions.
38 changes: 37 additions & 1 deletion src/common/controllers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Credentials } from '../../homeAssistant';
import HomeAssistant from '../../homeAssistant/HomeAssistant';
import { BaseNode } from '../../types/nodes';
import { EntityConfigNode } from '../../nodes/entity-config';
import { BaseNode, ServerNode } from '../../types/nodes';
import ClientEvents from '../events/ClientEvents';
import JSONataService from '../services/JSONataService';
import NodeRedContextService from '../services/NodeRedContextService';
import TypedInputService from '../services/TypedInputService';
import EventsStatus from '../status/EventStatus';

/**
* Create some of the dependencies needed for a BaseNode controller.
Expand Down Expand Up @@ -32,3 +36,35 @@ export function createControllerDependencies(
typedInputService,
};
}

export function createExposeAsControllerDependences({
exposeAsConfigNode,
homeAssistant,
node,
serverConfigNode,
}: {
exposeAsConfigNode?: EntityConfigNode;
homeAssistant: HomeAssistant;
node: BaseNode;
serverConfigNode: ServerNode<Credentials>;
}) {
const controllerDeps = createControllerDependencies(node, homeAssistant);

const clientEvents = new ClientEvents({
node,
emitter: homeAssistant.eventBus,
});

const status = new EventsStatus({
clientEvents,
config: serverConfigNode.config,
exposeAsEntityConfigNode: exposeAsConfigNode,
node,
});

return {
...controllerDeps,
exposeAsConfigNode,
status,
};
}
11 changes: 10 additions & 1 deletion src/editor/exposenode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EditorRED } from 'node-red';

import { NO_VERSION, NodeType } from '../const';
import { NO_VERSION, NodeType, ValueIntegrationMode } from '../const';
import { HassExposedConfig, HassNodeProperties } from './types';
import * as haUtils from './utils';

Expand Down Expand Up @@ -106,6 +106,10 @@ export function init(n: HassNodeProperties) {
break;
}
});

if (isEntityNode()) {
$('#node-input-mode').on('change', toggleExposeAsForListenMode);
}
}

function render() {
Expand Down Expand Up @@ -259,6 +263,11 @@ function renderAlert(type: NodeType) {
$('#integrationAlert').toggle(!integartionValid);
}

export function toggleExposeAsForListenMode() {
const mode = $('#node-input-mode').val() as ValueIntegrationMode;
$('#exposed-as-row').toggle(mode === ValueIntegrationMode.Listen);
}

// TODO: Can be removed when all nodes are migrated to Typescript
function toggleExposeAs() {
$('#node-input-exposeAsEntityConfig')
Expand Down
20 changes: 0 additions & 20 deletions src/nodes/number/NumberController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { NodeMessage } from 'node-red';

import InputOutputController, {
InputOutputControllerOptions,
InputProperties,
Expand Down Expand Up @@ -124,24 +122,6 @@ export default class NumberController extends InputOutputController<
}
}

public async onValueChange(value: number, previousValue?: number) {
if (isNaN(value)) return;

const message: NodeMessage = {};

await this.setCustomOutputs(
this.node.config.outputProperties,
message,
{
config: this.node.config,
value,
previousValue,
},
);
this.status.setSuccess(value.toString());
this.node.send(message);
}

// keep the number in range if min/max is set in the entity config
#getValidatedValue(value: number): number {
const min = Number(
Expand Down
29 changes: 29 additions & 0 deletions src/nodes/number/NumberOutputController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NodeMessage } from 'node-red';

import ExposeAsMixin from '../../common/controllers/ExposeAsMixin';
import OutputController from '../../common/controllers/OutputController';
import ValueEntityIntegration from '../../common/integration/ValueEntityIntegration';
import { NumberNode } from '.';

const ExposeAsController = ExposeAsMixin(OutputController<NumberNode>);
export default class NumberOutputController extends ExposeAsController {
protected integration?: ValueEntityIntegration;

public async onValueChange(value: number, previousValue?: number) {
if (!this.isEnabled || isNaN(value)) return;

const message: NodeMessage = {};

await this.setCustomOutputs(
this.node.config.outputProperties,
message,
{
config: this.node.config,
value,
previousValue,
},
);
this.status.setSuccess(value.toString());
this.node.send(message);
}
}
14 changes: 9 additions & 5 deletions src/nodes/number/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@
<span data-i18n="ha-number.label.mode"></span>
</label>
<select id="node-input-mode">
<option
value="listen"
data-i18n="ha-text.label.mode_option.listen"
></option>
<option value="listen" data-i18n="ha-text.label.mode_option.listen"></option>
<option value="get" data-i18n="ha-text.label.mode_option.get"></option>
<option value="set" data-i18n="ha-text.label.mode_option.set"></option>
</select>
Expand All @@ -37,4 +34,11 @@
<input type="hidden" id="node-input-valueType" />
</div>

<div class="form-row"><ol id="custom-outputs"></ol></div>
<div class="form-row">
<ol id="custom-outputs"></ol>
</div>

<div class="form-row" id="exposed-as-row">
<label for="node-input-exposeAsEntityConfig" data-i18n="ha-tag.label.expose_as"></label>
<input type="text" id="node-input-exposeAsEntityConfig" />
</div>
15 changes: 12 additions & 3 deletions src/nodes/number/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,16 @@ const NumberEditor: EditorNodeDef<NumberEditorNodeProperties> = {
value: '',
type: NodeType.EntityConfig,
// @ts-expect-error - DefinitelyTyped is missing this property
filter: (config) => config.entityType === 'number',
filter: (config) => config.entityType === EntityType.Number,
required: true,
},
exposeAsEntityConfig: {
value: '',
type: NodeType.EntityConfig,
// @ts-ignore - DefinitelyTyped is missing this property
filter: (config) => config.entityType === EntityType.Switch,
required: false,
},
mode: { value: ValueIntegrationMode.Listen },
value: { value: 'payload' },
valueType: { value: TypedInputTypes.Message },
Expand All @@ -72,11 +79,13 @@ const NumberEditor: EditorNodeDef<NumberEditorNodeProperties> = {
exposeNode.init(this);

saveEntityType(EntityType.Number);
saveEntityType(EntityType.Switch, 'exposeAsEntityConfig');
$('#dialog-form').prepend(ha.betaWarning(962));

const $valueRow = $('#node-input-value').parent();
$('#node-input-mode').on('change', function (this: HTMLSelectElement) {
$valueRow.toggle(this.value === ValueIntegrationMode.Set);
$('#node-input-value')
.parent()
.toggle(this.value === ValueIntegrationMode.Set);
$('#node-input-inputs').val(
this.value === ValueIntegrationMode.Listen ? 0 : 1,
);
Expand Down
93 changes: 63 additions & 30 deletions src/nodes/number/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import Joi from 'joi';

import { createControllerDependencies } from '../../common/controllers/helpers';
import {
createControllerDependencies,
createExposeAsControllerDependences,
} from '../../common/controllers/helpers';
import Events from '../../common/events/Events';
import { IntegrationEvent } from '../../common/integration/Integration';
import InputService, { NodeInputs } from '../../common/services/InputService';
import Status from '../../common/status/Status';
import { TypedInputTypes, ValueIntegrationMode } from '../../const';
import { RED } from '../../globals';
import { migrate } from '../../helpers/migrate';
import { getConfigNodes } from '../../helpers/node';
import { getConfigNodes, getExposeAsConfigNode } from '../../helpers/node';
import { getHomeAssistant } from '../../homeAssistant/index';
import {
BaseNode,
EntityBaseNodeProperties,
OutputProperty,
} from '../../types/nodes';
import NumberController from './NumberController';
import NumberOutputController from './NumberOutputController';

export interface NumberNodeProperties extends EntityBaseNodeProperties {
mode: ValueIntegrationMode;
value: string;
valueType: string;
outputProperties: OutputProperty[];
exposeAsEntityConfig: string;
}

export interface NumberNode extends BaseNode {
Expand Down Expand Up @@ -63,36 +68,64 @@ export default function numberNode(

const { entityConfigNode, serverConfigNode } = getConfigNodes(this);
const homeAssistant = getHomeAssistant(serverConfigNode);
const status = new Status({
config: serverConfigNode.config,
node: this,
});

const controllerDeps = createControllerDependencies(this, homeAssistant);
const inputService = new InputService<NumberNodeProperties>({
inputs,
nodeConfig: this.config,
schema: inputSchema,
});
switch (this.config.mode) {
case ValueIntegrationMode.Get:
case ValueIntegrationMode.Set:
{
const controllerDeps = createControllerDependencies(
this,
homeAssistant,
);
const status = new Status({
config: serverConfigNode.config,
node: this,
});
const inputService = new InputService<NumberNodeProperties>({
inputs,
nodeConfig: this.config,
schema: inputSchema,
});
entityConfigNode.integration.setStatus(status);

// eslint-disable-next-line no-new
new NumberController({
inputService,
integration: entityConfigNode.integration,
node: this,
status,
...controllerDeps,
});
}
break;
case ValueIntegrationMode.Listen:
{
const exposeAsConfigNode = getExposeAsConfigNode(
this.config.exposeAsEntityConfig,
);
const controllerDeps = createExposeAsControllerDependences({
exposeAsConfigNode,
homeAssistant,
node: this,
serverConfigNode,
});

entityConfigNode.integration.setStatus(status);
const controller = new NumberController({
inputService,
integration: entityConfigNode.integration,
node: this,
status,
...controllerDeps,
});
const controller = new NumberOutputController({
node: this,
...controllerDeps,
});
controller.setExposeAsConfigNode(exposeAsConfigNode);

if (this.config.mode === ValueIntegrationMode.Listen) {
const entityConfigEvents = new Events({
node: this,
emitter: entityConfigNode,
});
entityConfigEvents.setStatus(status);
entityConfigEvents.addListener(
IntegrationEvent.ValueChange,
controller.onValueChange.bind(controller),
);
const entityConfigEvents = new Events({
node: this,
emitter: entityConfigNode,
});
entityConfigEvents.setStatus(controllerDeps.status);
entityConfigEvents.addListener(
IntegrationEvent.ValueChange,
controller.onValueChange.bind(controller),
);
}
break;
}
}

0 comments on commit e7441e1

Please sign in to comment.