How to handle IDENTIFY request in Google Smart Home Local Fulfilment for multi channel device? - iot

I am trying to implement Local Fulfillment in my Google Smart Home Action.
some of my devices have 2 channels (dual relay switch).
Dual channel device is shown in Google Home app as two separate devices(light-test123123_0 and light-test123123_1) and it works just fine.
I have added customData and otherDevicesIds to my SYNC response to enable local fultilment and built an local app using #google/local-home-sdk. Google Home identifies Single channel devices and sends commands locally.
example of SYNC response body:
{
"response": {
"payload": {
"devices": [
{
"traits": [
"action.devices.traits.OnOff",
"action.devices.traits.Brightness"
],
"customData": {
"path": "/local-fulfil/0/",
"port": 3000
},
"name": {
"name": "light",
"nicknames": [
"light"
],
"defaultNames": [
"light"
]
},
"id": "light-test123123_0",
"type": "action.devices.types.LIGHT",
"deviceInfo": {
"hwVersion": "LIGHT",
"model": "LIGHT",
"swVersion": "1.0",
"manufacturer": "MANUFATURER"
},
"attributes": {},
"willReportState": false,
"otherDeviceIds": [
{
"deviceId": "light-test123123_0"
},
{
"deviceId": "light-test123123"
}
]
},
{
"name": {
"nicknames": [
"light"
],
"defaultNames": [
"light"
],
"name": "light"
},
"deviceInfo": {
"swVersion": "1.0",
"hwVersion": "LIGHT",
"manufacturer": "MANUFATURER",
"model": "LIGHT"
},
"attributes": {},
"otherDeviceIds": [
{
"deviceId": "light-test123123_1"
},
{
"deviceId": "light-test123123"
}
],
"willReportState": false,
"traits": [
"action.devices.traits.OnOff",
"action.devices.traits.Brightness"
],
"id": "light-test123123_1",
"customData": {
"port": 3000,
"path": "/local-fulfil/1/"
},
"type": "action.devices.types.LIGHT"
}
],
"agentUserId": "RuRHIPWpD5W23iGiU81A5PoTKqB2"
},
"requestId": "16772679358918515269"
}
}
The problems appeared when I have started the local SDK implementation because even if my dual channel device sends 2 separate UDP packets with different IDs mentioned above. Google ignores one of them. Looks like it is not possible to have 2 devices with same IP address.
Here is the IDENTIFY request body:
{
"requestId": "88DB84992F074FF0B408D8383CF198C4",
"inputs": [
{
"intent": "action.devices.IDENTIFY",
"payload": {
"device": {
"udpScanData": {
"data": "6C696768742D6475616C2D3132335F30"
}
},
"structureData": {}
}
}
],
"devices": [
{
"id": "light-dual-123_0",
"customData": {
"path": "/local-fulfil/0/",
"port": 3000
}
},
{
"id": "light-dual-123_1",
"customData": {
"path": "/local-fulfil/1/",
"port": 3000
}
}
]
}
and response:
{
"intent": "action.devices.IDENTIFY",
"requestId": "88DB84992F074FF0B408D8383CF198C4",
"payload": {
"device": {
"id": "",
"verificationId": "light-test123123_0"
}
}
}
I have written a basic app that sends UDP broadcast and it outputs the next data:
sent broadcast packet
192.168.1.235:8888 sent this: light-test123123_0
192.168.1.235:8888 sent this: light-test123123_1
That shows how my device acts when it receives a broadcast packet.
Is there a way to fix this?

The Local Fulfillment platform currently (as of Local Home SDK 1.4) uniquely identify the device by their network address, so it won't be able to handle multiple broadcast response coming from the same physical connected device.
You can however have one device acting as a proxy for multiple end-devices, this is done by:
returning setting the isProxy flag to true in the IDENTIFY response:
https://developers.google.com/assistant/smarthome/develop/local#hub-identify
handling the REACHABLE_DEVICES intent to return all the end-device verificationId:
https://developers.google.com/assistant/smarthome/develop/local#hub-identify
The Local Home SDK sample includes a virtual device that can be run in this configuration:
https://github.com/actions-on-google/smart-home-local#set-up-the-virtual-device

Related

Twilio Voice - allow current caller to listen to previous caller's recorded voicemail

Hoping someone can point me in the right direction. I'd like to be able to do this via Twilio Studio. If not, I can learn TwiML. That's about as far as my brain will stretch.
I've made a simple flow in Twilio Studio that enables the caller to record a voicemail. I would like to add an option for the current caller to be able to play the previous caller's recorded voicemail. I think I need to use a Say/Play widget for this. What do I need to use for the "URL of audio file" so that the previous recorded voicemail is played? I assume this URL will change every time that a caller leaves a voicemail, so it'll need to auto-update. Can I use "RecordingURL" somehow? Is there a solution using TwiML? Any help appreciated
Thanks!
Without a way to keep state between the call, this is not possible. The best way would be to have some type of DB you can store the recording SID and reference it in a future flow. You can use a tool like Twilio Sync to do this or Airtable, but it does require code.
I can't think of a way to do this without involving some coding.
Alternate ways are to list the recording records and pull the most recent one off the list but not ideal since you don't know when the last recording occurred.
Another approach is to modify the webhook associated with your Twilio phone number for that Studio Flow to pass in the last Recording SID into your flow and you use that SID to dynamically construct the recording URL to playback, as shown in the JSON flow below (which you can import when creating a new Studio Flow).
Don't forget to set the recordingStatuscallback to the above Twilio Function which updates the phone number webhook to pass in the recording SID. You set your Recording Status Callback of your Record Voicemail Widget to point to your unique Function domain and function path (currently set to: https://magnolia-kitty-3234.twil.io/phUpdate).
Feel free to improve on the below and share.
Twilio Function Code
exports.handler = function(context, event, callback) {
let client = context.getTwilioClient();
const telephoneNumAccountSid = "PN..."; \\ set this to your Phone Number SID
const accountSid = event.AccountSid;
const studioFlowSid = "FW..."; \\ set this to your Studio Flow SID
const webhookUrl = `https://webhooks.twilio.com/v1/Accounts/${accountSid}/Flows/${studioFlowSid}`;
const recordingSid = event.RecordingSid;
client
.incomingPhoneNumbers(telephoneNumAccountSid)
.update({ voiceUrl: `${webhookUrl}?recording=${recordingSid}`, voiceFallbackUrl: `${webhookUrl}?recording=${recordingSid}` })
.then (result => {
console.log(result.voiceUrl);
callback(null, "success");
})
.catch(err => {
console.log(err);
callback("error");
});
};
Studio Flow JSON
{
"description": "A New Flow",
"states": [
{
"name": "Trigger",
"type": "trigger",
"transitions": [
{
"event": "incomingMessage"
},
{
"next": "set_variables_1",
"event": "incomingCall"
},
{
"event": "incomingRequest"
}
],
"properties": {
"offset": {
"x": 0,
"y": 0
}
}
},
{
"name": "split_1",
"type": "split-based-on",
"transitions": [
{
"next": "say_play_2",
"event": "noMatch"
},
{
"next": "gather_1",
"event": "match",
"conditions": [
{
"friendly_name": "{{trigger.call.recording}}",
"arguments": [
"{{trigger.call.recording}}"
],
"type": "is_not_blank",
"value": "Is Not Blank"
}
]
}
],
"properties": {
"input": "{{trigger.call.recording}}",
"offset": {
"x": 110,
"y": 350
}
}
},
{
"name": "say_play_1",
"type": "say-play",
"transitions": [
{
"event": "audioComplete"
}
],
"properties": {
"play": "https://api.twilio.com/2010-04-01/Accounts/{{flow.variables.accountSid}}/Recordings/{{flow.variables.recording}}.mp3",
"offset": {
"x": 450,
"y": 1110
},
"loop": 1
}
},
{
"name": "set_variables_1",
"type": "set-variables",
"transitions": [
{
"next": "split_1",
"event": "next"
}
],
"properties": {
"variables": [
{
"value": "{{trigger.call.recording}}",
"key": "recording"
},
{
"value": "{{trigger.call.AccountSid}}",
"key": "accountSid"
}
],
"offset": {
"x": 60,
"y": 170
}
}
},
{
"name": "say_play_2",
"type": "say-play",
"transitions": [
{
"next": "record_voicemail_1",
"event": "audioComplete"
}
],
"properties": {
"voice": "Polly.Joanna-Neural",
"offset": {
"x": -120,
"y": 650
},
"loop": 1,
"say": "Please leave a message at the beep!",
"language": "en-US"
}
},
{
"name": "record_voicemail_1",
"type": "record-voicemail",
"transitions": [
{
"event": "recordingComplete"
},
{
"event": "noAudio"
},
{
"event": "hangup"
}
],
"properties": {
"transcribe": false,
"offset": {
"x": -120,
"y": 860
},
"trim": "trim-silence",
"play_beep": "true",
"recording_status_callback_url": "https://magnolia-kitty-3234.twil.io/phUpdate",
"timeout": 5,
"max_length": 3600
}
},
{
"name": "gather_1",
"type": "gather-input-on-call",
"transitions": [
{
"next": "split_2",
"event": "keypress"
},
{
"event": "speech"
},
{
"event": "timeout"
}
],
"properties": {
"number_of_digits": 1,
"speech_timeout": "auto",
"offset": {
"x": 300,
"y": 650
},
"loop": 1,
"finish_on_key": "#",
"say": "There is a previous recording, press 1 if you want to listen to it or 2 if you want to leave a new voicemail.",
"stop_gather": true,
"gather_language": "en",
"profanity_filter": "true",
"timeout": 5
}
},
{
"name": "split_2",
"type": "split-based-on",
"transitions": [
{
"event": "noMatch"
},
{
"next": "say_play_1",
"event": "match",
"conditions": [
{
"friendly_name": "If value equal_to 1",
"arguments": [
"{{widgets.gather_1.Digits}}"
],
"type": "equal_to",
"value": "1"
}
]
},
{
"next": "say_play_2",
"event": "match",
"conditions": [
{
"friendly_name": "If value equal_to 2",
"arguments": [
"{{widgets.gather_1.Digits}}"
],
"type": "equal_to",
"value": "2"
}
]
}
],
"properties": {
"input": "{{widgets.gather_1.Digits}}",
"offset": {
"x": 280,
"y": 870
}
}
}
],
"initial_state": "Trigger",
"flags": {
"allow_concurrent_calls": true
}
}
Alan

Graph Pagination in Logic Apps

I'm trying to fetch all users from a specific group via an HTTP connector, a registered app, and Microsoft Graph.
The registered app has Directory.Read.All permissions.
My idea is that I'm calling the nextLink as long as it's there while appending all of the fetched users' userPrincipalName to an array eventually filling the array with all users of the group.
My Logic App looks like this:
Unfortunately, I'm just 1 reputation short of posting images, please forgive. The 3 links should provide an overview of the structure of my app.
First, nextLink is initialized to the first Graph API endpoint. This variable is set to the current nextLink through each iteration of the until loop.
Second, For the purpose of this exercise, I only get the top 5. I know there are only 9 users:
Lastly, I call the union method on the "users" array that I initialized earlier and the "value" array from the HTTP get method, to get one single array consisting of all users:
The issue is that the HTTP action always returns the same top 5 users. I've checked that the nextLink provided in the first HTTP GET call to Graph, is correct by copying it from the Runs history and pasting it into Microsoft Graph Explorer and there the next 4 users are correctly returned.
I also made sure that, for each iteration in the until loop, I call the Graph API with the nextLink from the previous iteration as expected.
The nextLink returned inside of the Logic App is exactly the same when I test it in Graph Explorer, but the same nextLink returns 2 different results when called from Graph Explorer and inside my Logic App.
Why is the result always the same top 5 users and not the next 4 users as expected?
If not sure about the reason why you will get this issue, but based on your requirement, I did a sample below:
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Initialize_variable": {
"inputs": {
"variables": [
{
"name": "GetGroupUrl",
"type": "string",
"value": "https://graph.microsoft.com/v1.0/groups/<your group id>/members?$select=userPrincipalName&$top=5"
}
]
},
"runAfter": {},
"type": "InitializeVariable"
},
"Initialize_variable_2": {
"inputs": {
"variables": [
{
"name": "users",
"type": "array"
}
]
},
"runAfter": {
"Initialize_variable": [
"Succeeded"
]
},
"type": "InitializeVariable"
},
"Until": {
"actions": {
"Compose": {
"inputs": "#union(variables('users'),body('HTTP')['value'])",
"runAfter": {
"HTTP": [
"Succeeded"
]
},
"type": "Compose"
},
"HTTP": {
"inputs": {
"authentication": {
"audience": "https://graph.microsoft.com",
"clientId": "<app id>",
"secret": "<app secret>",
"tenant": "<your secret>",
"type": "ActiveDirectoryOAuth"
},
"method": "GET",
"uri": "#variables('GetGroupUrl')"
},
"runAfter": {},
"type": "Http"
},
"Set_variable": {
"inputs": {
"name": "GetGroupUrl",
"value": "#{if(equals(body('HTTP')?['#odata.nextLink'], null),null,body('HTTP')['#odata.nextLink'])}"
},
"runAfter": {
"Compose": [
"Succeeded"
]
},
"type": "SetVariable"
}
},
"expression": "#equals(variables('GetGroupUrl'), '')",
"limit": {
"count": 60,
"timeout": "PT1H"
},
"runAfter": {
"Initialize_variable_2": [
"Succeeded"
]
},
"type": "Until"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"manual": {
"inputs": {
"method": "GET",
"schema": {
"properties": {
"text": {
"type": "string"
}
},
"type": "object"
}
},
"kind": "Http",
"type": "Request"
}
}
},
"parameters": {}
}
You can just replace the params with your own and paste it into your logic app code view and test it .
It works for me, as you can see , each request results are different :
Hope it helps .
This issue solved by OP self, this issue is due to queries in request URL , copy OP's comment as an answer :
After fiddling a bit more around with what each of you providing I
found a solution. It seems that when the query arguments are passed to
the HTTP GET outside of the endpoint itself (meaning in the "queries"
field inside of the block) it seems to keep overriding the nextLink.
When writing the endpoint URL out entirely with the odata parameters,
it works as intended.

Microsoft Graph API group folder invite response is wrong

I have been trying to grant a single user (possibly outside of the organization) access to a folder used by a shared group in Share Point The Microsoft Graph Docs state that the response should have an Id in it.
If I use a personal oneDrive account, and make the following request:
POST v1.0
https://graph.microsoft.com/v1.0/groups/{groupId}/drive/items/{folderId}/invite
body: {
"requireSignIn": true,
"sendInvitation": true,
"roles": [ "read"],
"recipients": [
{ "email": "{myPersonalEmail}" },
],
"message": "testing giving permission to self"
}
I get the following response:
{
"#odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(permission)",
"value": [
{
"#odata.type": "#microsoft.graph.permission",
"id": "{permissionID}",
"roles": [
"read"
],
"grantedTo": {
"user": {
"email": "{myPersonalEmail}",
"id": "{responseID}",
"displayName": "Sarah"
}
}
}
]
}
However, we setup a different SharePoint account that when I make the same request https://graph.microsoft.com/v1.0/groups/{groupId}/drive/items/{folderId}/invite
I get a completely different response:
{
"#odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(permission)",
"value": [
{
"#odata.type": "#microsoft.graph.permission",
"roles": [
"read"
],
"invitation": {
"signInRequired": true
},
"link": {
"type": "view",
"webUrl": "{shareURL}"
}
}
]
}
This is so bizarre to me, because we are hitting the same endpoint. It makes me think that maybe there are some site configurations that are making these two responses differ.
Does anyone know why we would be getting a different results from the same endpoint?

Atlassian Connect Express: Credentials rejected at connect-ace.atlassian.net

I'm evaluating atlassian-connect-express and just created an app
boilerplate with "atlassian-connect new", and then deployed it via ngrok
to my Jira dev account.
That works fine, but when I try to use the file "credentials.json" with
my account data, the plug starts with the error message:
Failed to register with host https‍://michael%40...:[My
password]#connect-ace.atlassian.net (401)
Add-on not registered; no compatible hosts detected
I get a similar message when I go to the url connect-ace.atlassian.net
Here my atlassian-connect.json
{
"key": "my-add-on",
"name": "Ping Pong",
"description": "My very first add-on",
"vendor": {
"name": "Angry Nerds",
"url": "https://www.atlassian.com/angrynerds"
},
"baseUrl": "https://xxxxxxx.ngrok.io",
"links": {
"self": "https://xxxxxxxx.ngrok.io/atlassian-connect.json",
"homepage": "https://xxxxxxx.ngrok.io/atlassian-connect.json"
},
"authentication": {
"type": "jwt"
},
"lifecycle": {
"installed": "/installed"
},
"scopes": [
"READ"
],
"modules": {
"generalPages": [
{
"key": "hello-world-page-jira",
"location": "system.top.navigation.bar",
"name": {
"value": "Hello World"
},
"url": "/hello-world",
"conditions": [{
"condition": "user_is_logged_in"
}]
},
{
"key": "hello-world-page-confluence",
"location": "system.header/left",
"name": {
"value": "Hello World"
},
"url": "/hello-world",
"conditions": [{
"condition": "user_is_logged_in"
}]
}
]
}
}
and my credatials.json
{
"hosts": {
"connect-ace.atlassian.net": {
"product": "jira",
"username": "michael#---",
"password": "---password---"
}
}
}
How can I get my dev account working with connect-ace?
Two things to check:
1 - Check the if the credentials.json has correct values. host url should start with https://. Password is the api token generated from here.
{
"hosts": {
"https://<your atlassian site name>.atlassian.net": {
"product": "jira",
"username": "<jira user name>",
"password": "<Token created from https://id.atlassian.com/manage/api-tokens>"
}
}
}
2- Enable development mode at Settings -> Apps -> Manage Apps -> Settings. (This is required if the app is not published in marketplace)
This error is a default from atlas-connect
Print error image
In atlasssian-connect.json
replace this :
"lifecycle": {
"installed": "/installed"
},
for this:
"lifecycle": {
"installed": "installed"
},
Have you tried spawning your own cloud instance and testing it there? If not yet, try creating one here. Once successfully registered, in your credentials.json, change "connect-ace" with your baseUrl/sitename and with the right credentials, it should automatically install it for you.

Webhook input not available in scripting step?

I have an internal app that uses a webhook listener and some scripting to manipulate the input data. I'm posting this to it:
curl -X POST -d '{
"assignment_id": 12345,
"updated_custom_fields": [{
"name": "RNVIDAYEBB",
"value": "updated!"
},
{
"name": "QUFTXSIBYA",
"value": "and me too"
}
],
"custom_fields": [{
"id": 981,
"name": "RDEXDPVKRD",
"fields": [
{
"id": 4096,
"name": "RNVIDAYEBB",
"default": "EDJEAJICYW",
"required": true,
"value": "Blah"
},
{
"id": 4097,
"name": "QUFTXSIBYA",
"default": "",
"required": true,
"value": ""
}]
}]
}' "https://hooks.zapier.com/hooks/catch/......"
My script is as follows:
update_custom_fields_by_name_pre_write: function(bundle) {
var updatedFields = _.map(bundle.request.data.custom_fields, function(group) {
return _.map(group.fields, function(field) {
return _.extend(field, _.findWhere(bundle.request.data.updated_custom_fields, { name: field.name} ));
});
});
bundle.request.data = updatedFields;
return bundle.request;
}
I know that the merging logic is good, but it appears that the custom_fields and updated_custom_fields arrays are not present in the bundle.request.data object. Anyone know how to get access to them in the script?
It seems like you should be using update_custom_fields_by_name_catch_hook to capture the incoming static webhook data (instead of _pre_write). If you use that, you can capture the data within bundle.cleaned_request.custom_fields and bundle.cleaned_request.updated_custom_fields.

Resources