Skip to content

Commit

Permalink
fix(call-service): Send targets in the data field so HA doesn't autom…
Browse files Browse the repository at this point in the history
…atically convert them to arrays

Closes #584
Closes #582
  • Loading branch information
zachowj committed Feb 17, 2022
1 parent 581992b commit d085ef4
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 45 deletions.
3 changes: 3 additions & 0 deletions locales/en-US/call-service.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"queue_none": "don't queue messages",
"service": "Service",
"show_debug": "Show debug information"
},
"error": {
"invalid_entity_id": "Invalid entity id format"
}
}
}
67 changes: 43 additions & 24 deletions src/nodes/call-service/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ const nodeOptions = {
config: {
domain: {},
service: {},
target: {},
areaId: {},
deviceId: {},
entityId: {},
data: {},
dataType: (nodeDef) => nodeDef.dataType || 'json',
mergeContext: {},
Expand Down Expand Up @@ -60,6 +62,17 @@ class CallService extends EventsNode {
const render = generateRenderTemplate(message, context, states);
const apiDomain = render(payloadDomain || config.domain);
const apiService = render(payloadService || config.service);

if (!apiDomain || !apiService) {
done(
`call service node is missing api "${
!apiDomain ? 'domain' : 'service'
}" property, not found in config or payload`
);
this.status.setFailed('Error');
return;
}

const apiTarget = this.getTargetData(payloadTarget, message);
let configData;
if (config.dataType === 'jsonata' && config.data) {
Expand All @@ -75,17 +88,11 @@ class CallService extends EventsNode {
} else {
configData = render(config.data, config.mustacheAltTags);
}
const apiData = this.getApiData(payloadData, configData);

if (!apiDomain || !apiService) {
done(
`call service node is missing api "${
!apiDomain ? 'domain' : 'service'
}" property, not found in config or payload`
);
this.status.setFailed('Error');
return;
}
const apiData = this.getApiData(
payloadData,
this.tryToObject(configData),
apiTarget
);

this.node.debug(
`Calling Service: ${JSON.stringify({
Expand All @@ -100,7 +107,6 @@ class CallService extends EventsNode {
apiDomain,
apiService,
apiData: Object.keys(apiData).length ? apiData : undefined,
apiTarget: Object.keys(apiTarget).length ? apiTarget : undefined,
message,
done,
send,
Expand Down Expand Up @@ -142,7 +148,7 @@ class CallService extends EventsNode {
}
}

getApiData(payload = {}, config = {}) {
getApiData(payload = {}, config = {}, target = {}) {
let contextData = {};

// Calculate payload to send end priority ends up being 'Config, Global Ctx, Flow Ctx, Payload' with right most winning
Expand All @@ -155,12 +161,12 @@ class CallService extends EventsNode {
contextData = { ...globalVal, ...flowVal };
}

return { ...this.tryToObject(config), ...contextData, ...payload };
return { ...config, ...contextData, ...payload, ...target };
}

getTargetData(payload, message) {
const context = this.node.context();
const states = this.homeAssistant.getStates();
const states = this.homeAssistant && this.homeAssistant.getStates();
const render = generateRenderTemplate(message, context, states);

const map = {
Expand All @@ -170,10 +176,10 @@ class CallService extends EventsNode {
};
const configTarget = {};

Object.keys(this.nodeConfig?.target).forEach((key) => {
Object.keys(map).forEach((key) => {
const prop = map[key];
configTarget[prop] = this.nodeConfig.target[key]
? [...this.nodeConfig.target[key]]
configTarget[prop] = this.nodeConfig[key]
? [...this.nodeConfig[key]]
: undefined;
if (Array.isArray(configTarget[prop])) {
// If length is 0 set it to undefined so the target can be overridden from the data field
Expand Down Expand Up @@ -210,14 +216,19 @@ class CallService extends EventsNode {
}
}
});
return merge(configTarget, payload);
const targets = merge(configTarget, payload);
// remove undefined values
Object.keys(targets).forEach(
(key) => targets[key] === undefined && delete targets[key]
);

return targets;
}

async processInput({
apiDomain,
apiService,
apiData,
apiTarget,
message,
done,
send,
Expand All @@ -227,7 +238,6 @@ class CallService extends EventsNode {
const data = {
domain: apiDomain,
service: apiService,
target: apiTarget,
data: apiData,
};
this.debugToClient(data);
Expand All @@ -236,8 +246,7 @@ class CallService extends EventsNode {
await this.homeAssistant.callService(
apiDomain,
apiService,
apiData,
apiTarget
apiData
);
} catch (err) {
// ignore 'connection lost' error on homeassistant.restart
Expand All @@ -246,6 +255,16 @@ class CallService extends EventsNode {
apiService !== 'restart' &&
selectn('error.code', err) !== 3
) {
// When entity id is not fomatted correctly, the error is not very helpful
if (
err.code === 'unknown_error' &&
err.message ===
'not enough values to unpack (expected 2, got 1)'
) {
err.message = this.RED._(
'api-call-service.error.invalid_entity_id'
);
}
done(`Call-service error. ${err.message ? err.message : ''}`);
this.status.setFailed('API Error');
return;
Expand Down
35 changes: 15 additions & 20 deletions src/nodes/call-service/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ import { displayValidTargets, getTarget, populateTargets } from './targets';

declare const RED: EditorRED;

interface Target {
areaId?: string[];
deviceId?: string[];
entityId?: string[];
}

interface CallServiceEditorNodeProperties extends EditorNodeProperties {
server: any;
version: number;
Expand All @@ -31,13 +25,15 @@ interface CallServiceEditorNodeProperties extends EditorNodeProperties {
service: string;
data: string;
dataType: string;
target: Target;
areaId?: string[];
deviceId?: string[];
entityId?: string[];
mergeContext: string;
mustacheAltTags: boolean;
queue: string;
outputProperties: OutputProperty[];
// deprecated
entityId?: string;
target: undefined;
output_location?: string;
output_location_type?: string;
service_domain?: string;
Expand Down Expand Up @@ -68,13 +64,9 @@ const CallServiceEditor: EditorNodeDef<CallServiceEditorNodeProperties> = {
debugenabled: { value: false },
domain: { value: '' },
service: { value: '' },
target: {
value: {
areaId: [],
deviceId: [],
entityId: [],
},
},
areaId: { value: [] },
deviceId: { value: [] },
entityId: { value: [] },
data: { value: '' },
dataType: { value: 'jsonata' },
mergeContext: { value: '' },
Expand All @@ -85,7 +77,7 @@ const CallServiceEditor: EditorNodeDef<CallServiceEditorNodeProperties> = {
},
queue: { value: 'none' },
// deprecated
entityId: { value: undefined },
target: { value: undefined },
output_location: { value: undefined },
output_location_type: { value: undefined },
service_domain: { value: undefined },
Expand Down Expand Up @@ -189,9 +181,9 @@ const CallServiceEditor: EditorNodeDef<CallServiceEditorNodeProperties> = {
populateDomains(this.domain);
populateServices(this.service);
populateTargets({
entityId: this.target.entityId,
areaId: this.target.areaId,
deviceId: this.target.deviceId,
entityId: this.entityId,
areaId: this.areaId,
deviceId: this.deviceId,
});
displayValidTargets();
updateServiceSelection();
Expand Down Expand Up @@ -220,7 +212,10 @@ const CallServiceEditor: EditorNodeDef<CallServiceEditorNodeProperties> = {
this.outputProperties = haOutputs.getOutputs();
this.domain = $('#domain').select2('data')?.[0]?.id;
this.service = $('#service').select2('data')?.[0]?.id;
this.target = getTarget();
const { areaId, deviceId, entityId } = getTarget();
this.areaId = areaId;
this.deviceId = deviceId;
this.entityId = entityId;
},
};

Expand Down
15 changes: 15 additions & 0 deletions src/nodes/call-service/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ export default [
mergecontext: undefined,
};

return newSchema;
},
},
{
version: 5,
up: (schema: any) => {
const newSchema = {
...schema,
version: 5,
areaId: schema.target?.areaId,
deviceId: schema.target?.deviceId,
entityId: schema.target?.entityId,
target: undefined,
};

return newSchema;
},
},
Expand Down
37 changes: 36 additions & 1 deletion test/migrations/call-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ const VERSION_4 = {
service_domain: undefined,
mergecontext: undefined,
};
const VERSION_5 = {
...VERSION_4,
version: 5,
areaId: [],
deviceId: [],
entityId: ['entity.id1', 'entity.id2'],
target: undefined,
};

describe('Migrations - Call Service Node', function () {
describe('Version 0', function () {
Expand Down Expand Up @@ -200,8 +208,35 @@ describe('Migrations - Call Service Node', function () {
});
});
});
describe('Version 5', function () {
let migrate = null;
before(function () {
migrate = migrations.find((m) => m.version === 5);
});
it('should update version 4 to version 5', function () {
const migratedSchema = migrate.up(VERSION_4);

expect(migratedSchema).to.eql(VERSION_5);
});
it('should move targets to the base object', function () {
const schema = {
...VERSION_4,
target: {
areaId: ['living_room'],
deviceId: ['1234'],
entityId: ['sun.sun'],
},
};
const migratedSchema = migrate.up(schema);

expect(migratedSchema.areaId).to.eql(['living_room']);
expect(migratedSchema.deviceId).to.eql(['1234']);
expect(migratedSchema.entityId).to.eql(['sun.sun']);
expect(migratedSchema.target).to.eql(undefined);
});
});
it('should update an undefined version to current version', function () {
const migratedSchema = migrate(VERSION_UNDEFINED);
expect(migratedSchema).to.eql(VERSION_4);
expect(migratedSchema).to.eql(VERSION_5);
});
});

0 comments on commit d085ef4

Please sign in to comment.