@rob-loosx This is an excellent idea, much more intuitive than "push and twist".
-
-
Hey team,
I use an automation platform called Latenode. It's basically like Zapier but heaps cheaper...
I'm playing around with the hub SDK atm. I keep getting a TlsFailure error when sending the request to the Latenode webhook URL though. The SSL cert seems to be all legit on the Latenode end.
d4000b30-10c7-4ab5-995a-2eaebbe45686-image.pngThis is the webhook URL:
https://webhook.latenode.com/9786/dev/276ef231-6ae4-40f0-8643-e65aed2b29bb(I'll change it when I make the integration live so if that link is dead in a month or so and someone looks at it, it probs wont work).
Any ideas? I've tried adding rejectUnauthorized: false into the request, but that hasn't worked.
And this is my code:
var buttonManager = require("buttons"); var http = require("http"); var url = "https://webhook.latenode.com/9786/dev/276ef231-6ae4-40f0-8643-e65aed2b29bb"; function makeRequest(url, button, clickType) { var fullUrl = url + `?button_name=${encodeURIComponent(button.name)}&click_type=${encodeURIComponent(clickType)}&battery_status=${encodeURIComponent(button.batteryStatus)}`; http.makeRequest({ url: fullUrl, method: "GET", headers: {"Content-Type": "application/json"} }, function(err, res) { if (err) { console.log("Request error: " + err); } else if (res) { console.log("Request status: " + res.statusCode); if (res.statusCode === 308 && res.headers["Location"]) { // Follow the redirect console.log("Following redirect to: " + res.headers["Location"]); makeRequest(res.headers["Location"], button, clickType); } } else { console.log("Response is undefined"); } }); } buttonManager.on("buttonSingleOrDoubleClickOrHold", function(obj) { var button = buttonManager.getButton(obj.bdaddr); var clickType = obj.isSingleClick ? "click" : obj.isDoubleClick ? "double_click" : "hold"; makeRequest(url, button, clickType); }); console.log("Started"); -
My setup is that I have a Flic Twist associated with Flic LR Hub. The goal is to be able to play Sonos internet radio stations (note: not “Playlists,” which are much more natively supported) and to change the volume with the twist dial.
The challenge is that the native IOS/Flic app does volume-to-Sonos correctly (via the twist dial), but can’t trigger stations. IFTTT is way too laggy/slow for real time music, so I ruled that out after I got it “working.” HA can trigger stations and set fixed volume, but volume adjustments from the dial are an issue. So, in the immortal words of the internet: “Why Not Both?”
--> Volume happens via the flic app
--> Station selection happens via a webhook to Home AssistantAlso, the Twist has a single and a double click, so I want to play two different radio stations (local NPR + Sirius XM) which is a whole additional problem.
CURRENT VERSIONS
Flic Hubs LR = v4.3.5
Filc Twist = v2
Sonos S1 = v11.14
HA Core = 2024.10.1
HA Supervisor = 2024.10.0
HA Operating System = 13.1Here’s how to do it:
Set up your Flic Twist & Hub + Sonos & configure the twist to do volume on the kitchen, via the Flic IOS app Then, in Home Assistant, set up an automation (yaml below) to trigger the radio playlist (trick is the x-rincon-mp3radio prefix before the http URL) note that there need to be two separate actions in the automation if you want to set the volume and play a thing) - I wanted a consistent, set volume for this particular source. Still in HA, set up a webhook trigger to call that automation. It’s notable that the creation of the webhook creates a single-factor “secret” in the name of the webhook, flic-button-pressed-<redacted> which becomes the URI. Test the automation with curl on your laptop, to make sure that part is set up. This should cause the music to start. curl -X POST http://homeassistant.local:8123/api/webhook/flic-button-pressed-<redacted> Then, in the Flic IOS App, configure the flic button push trigger a URL hit to HA with the “http internet request” action, and paste in your HA webhook link. Repeat the whole process for flic double-click, with a second automationThere’s a GOTCHA on the media_content_id and URL prefixing. For starters, media_content_type: music should JFW. Don’t worry about that part. But for the streaming station, you’ll need that x-rincon-mp3radio prefix. There’s a nice little doc about how to extract the Sirius XM specific station URL here, but do use the x-rincon-mp3radio prefix again, and not the x-sonosapi-hls-static approach.
alias: Flic Button Pressed description: "" triggers: - trigger: webhook allowed_methods: - POST - PUT local_only: true webhook_id: flic-button-pressed-<redacted> actions: - action: media_player.volume_set data: volume_level: 0.3 target: area_id: kitchen device_id: 59dd4ccdec3d2qq42408305b8c9c3fa7 entity_id: media_player.sonos_room_name - action: media_player.play_media target: entity_id: media_player.sonos_room_name area_id: kitchen device_id: 59dd4ccdec3d2qq42408305b8c9c3fa7 data: media_content_type: music media_content_id: ""x-rincon-mp3radio://https://live-ftc-prod-device.streaming.siriusxm.com/v1/763a312c707<redacted>_v4.m3u8 -
What is it?
Hub TCP server for using the IR module via HTTP.
Both to record of new signals with a given name and to play them back.
Using net since SDK doesn't have the nodejs HTTP-module available.Why?
I really wanted to be able to call my IR module from my phone homescreen or Google Home (via IFTTT). I got it working so I wanted to share it if anyone else find it helpful 🙂The code is what could be called a POC, so code feedback and/or changes are totally welcome.
How does it work?
Record a signal GET {ip}:1338?record={name_of_signal} Plays a signal GET {ip}:1338?cmd={name_of_signal} Cancel recording GET {ip}:1338?cancelRecord=trueExample;
call GET 192.168.0.100:1338?record=volume_up Press Volume Up on remote towards IR module returns 200 OK Signal volume_up stored! call GET 192.168.0.100:1338?cmd=volume_up returns 200 OK Signal sent! Volume goes up 🙂The code
module.json
{ "name": "IRTCP", "version": "1.0.0" }main.js
// main.js const net = require("net"); const ir = require("ir"); const datastore = require("datastore"); const utils = require("./utils"); const respondToClient = function(c, statusCode, message) { c.write( "HTTP/1.1 " + statusCode + "\r\n" + "\r\n" ); c.write(message); c.end(); } const handleCmd = function(c, cmd) { datastore.get(cmd, function(err, strBuffer) { if (!err && strBuffer) { var signal = new Uint32Array(utils.str2ab(strBuffer)); if (!ArrayBuffer.isView(signal)) { return respondToClient(c, "422 Unprocessable Entity", "Unknown signal"); } ir.play(signal, function(err) { if (!err) { return respondToClient(c, "200 OK", "Signal sent"); } return respondToClient(c, "500 Internal Server Error", "Couldn't send signal"); }); } else { return respondToClient(c, "422 Unprocessable Entity", "Unknown cmd"); } }); } const handleRecord = function(c, name) { /* TODO: add a timeout for listening for complete */ ir.record(); console.log("recording signal..."); ir.on("recordComplete", function(signal) { datastore.put(name, utils.ab2str(signal.buffer), function(err) { if (!err) { console.log("Signal " + name + " stored!"); return respondToClient(c, "200 OK", "Signal " + name + " stored!"); } else { return respondToClient(c, "500 Internal Server Error", "Couldn't store signal"); } }); }); } var server = net.createServer(function(c) { console.log('server connected'); c.on('end', function() { console.log('server disconnected'); }); c.on('data', function(data) { console.log(data); var match = data.toString().match(/GET \/\?.[^ ]*/); if (!match) { return respondToClient(c, "403 Forbidden", "Forbidden method"); } var params = utils.parseParams(match[0].split("GET /?")[1]); if (params.cmd) { return handleCmd(c, params.cmd); } else if (params.record && params.record.length > 0) { return handleRecord(c, params.record); } else if (params.cancelRecord) { ir.cancelRecord(); console.log("recording canceled!"); return respondToClient(c, "200 OK", "Recording canceled!"); } else { return respondToClient(c, "403 Forbidden", "Forbidden params"); } }); }); server.listen(1338, function() { console.log('server bound', server.address().port); });utils.js
const ab2str = function(buf) { return String.fromCharCode.apply(null, new Uint16Array(buf)); } const str2ab = function(str) { var buf = new ArrayBuffer(str.length*2); var bufView = new Uint16Array(buf); for (var i=0, strLen=str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf; } const parseParams = function (s) { var obj = {}; var arr = s.split("&"); var temp; for (i = 0; i < arr.length; i++) { temp = arr[i].split("="); obj[temp[0]] = temp[1]; } return obj; }; exports.ab2str = ab2str; exports.str2ab = str2ab; exports.parseParams = parseParams;