Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users
    1. Home
    2. Popular
    Log in to post
    • All categories
    • All Topics
    • New Topics
    • Watched Topics
    • Unreplied Topics
    • All Time
    • Day
    • Week
    • Month
    • sam

      Stuck Matter Bridge
      Flic Hub • • sam

      5
      0
      Votes
      5
      Posts
      253
      Views

      sam

      @Emil yes like @mullz said I was trying to manage the bridge via the button>actions>Modify Matter Settings and tap to edit didn't work.

      Via the providers though its fine, didn't think to look there,

      Thanks 🙂

    • rickard

      Flic duo gesture and twist sensitivity
      Developers • sdk • • rickard

      3
      0
      Votes
      3
      Posts
      65
      Views

      rickard

      @rickard If anyone else wants me a chatgpt (98% chatgpt) did this

      This basically uses a twist motion as a variable speed adjuster that keeps going after hitting value from flic (intended). For that reason the virtual volume/lights/blinds should not have a limiter, instead you configure the limiter here in main.js

      All you need to do is set up a virtual adjuster in the flic app and this will log the output. You can always then send this over mqtt or such

      // rateDetentController.js // // Key fixes in this version: // 1) Soft recenter while neutral-latched (prevents UP/DOWN asymmetry) // 2) Debounced "ease into fine mode" so jitter does NOT trigger fine mode // // No behavior changes elsewhere. function clamp(x, min, max) { return x < min ? min : (x > max ? max : x); } function sign(x) { return x > 0 ? 1 : (x < 0 ? -1 : 0); } class RateDetentController { constructor(cfg) { this.cfg = cfg || {}; this.tickMs = this.cfg.tickMs || 333; // Neutral hysteresis this.deadbandEnter = typeof this.cfg.deadbandEnter === "number" ? this.cfg.deadbandEnter : 5; this.deadbandExit = typeof this.cfg.deadbandExit === "number" ? this.cfg.deadbandExit : 9; // Speed tiers this.tier1MaxOff = typeof this.cfg.tier1MaxOff === "number" ? this.cfg.tier1MaxOff : 25; this.tier2MaxOff = typeof this.cfg.tier2MaxOff === "number" ? this.cfg.tier2MaxOff : 50; this.tierHys = typeof this.cfg.tierHys === "number" ? this.cfg.tierHys : 3; // Output clamp this.minOutPct = typeof this.cfg.minOutPct === "number" ? this.cfg.minOutPct : 0; this.maxOutPct = typeof this.cfg.maxOutPct === "number" ? this.cfg.maxOutPct : 100; // --- NEW: soft recenter tuning --- this.neutralRecenterAlpha = typeof this.cfg.neutralRecenterAlpha === "number" ? this.cfg.neutralRecenterAlpha : 0.08; // 8% per update while resting // --- NEW: debounce for easing into fine mode --- this.easeConfirmMs = typeof this.cfg.easeConfirmMs === "number" ? this.cfg.easeConfirmMs : 200; // State this.centerInPct = null; this.actualOutPct = typeof this.cfg.initialOutPct === "number" ? this.cfg.initialOutPct : null; this.fineMode = false; this.lastDir = 0; this.lastSpeed = 0; this.currentDir = 0; this.currentSpeed = 0; this.neutralLatched = false; this.speedLatched = 0; // For debouncing easing this._easeCandidateSince = null; this._easeCandidateDir = 0; this._lastIntentKey = null; this._timer = setInterval(() => this._tick(), this.tickMs); } _desiredSpeed(absOff) { if (absOff <= this.tier1MaxOff) return 1; if (absOff <= this.tier2MaxOff) return 2; return 3; } _updateLatchedSpeed(desired, absOff) { if (this.speedLatched === 0) { this.speedLatched = desired; return; } if (this.speedLatched === 1) { if (desired >= 2 && absOff >= (this.tier1MaxOff + this.tierHys)) this.speedLatched = 2; if (desired === 3 && absOff >= (this.tier2MaxOff + this.tierHys)) this.speedLatched = 3; return; } if (this.speedLatched === 2) { if (absOff <= (this.tier1MaxOff - this.tierHys)) { this.speedLatched = 1; return; } if (desired === 3 && absOff >= (this.tier2MaxOff + this.tierHys)) { this.speedLatched = 3; return; } return; } if (this.speedLatched === 3) { if (absOff <= (this.tier2MaxOff - this.tierHys)) this.speedLatched = 2; } } _baseIntent(rawInPct) { if (this.centerInPct === null) { this.centerInPct = rawInPct; this.neutralLatched = true; this.speedLatched = 0; return { dir: 0, speed: 0, desiredSpeed: 0, reason: "center set" }; } const off = rawInPct - this.centerInPct; const absOff = Math.abs(off); // --- Sticky neutral with SOFT RECENTER --- if (this.neutralLatched) { if (absOff <= this.deadbandExit) { // soft recenter while resting this.centerInPct = this.centerInPct + this.neutralRecenterAlpha * (rawInPct - this.centerInPct); this.speedLatched = 0; return { dir: 0, speed: 0, desiredSpeed: 0, reason: "deadband (latched)" }; } this.neutralLatched = false; } else { if (absOff <= this.deadbandEnter) { this.neutralLatched = true; this.speedLatched = 0; return { dir: 0, speed: 0, desiredSpeed: 0, reason: "deadband (enter)" }; } } const dir = sign(off); const desiredSpeed = this._desiredSpeed(absOff); this._updateLatchedSpeed(desiredSpeed, absOff); const speed = (this.speedLatched === 0) ? 1 : this.speedLatched; return { dir, speed, desiredSpeed, reason: "detent" }; } _applyFineMode(base) { const { dir, speed, desiredSpeed } = base; const now = Date.now(); const directionChanged = (this.lastDir !== 0 && dir !== 0 && dir !== this.lastDir); const hitNeutralFromIntent = (this.lastSpeed > 0 && speed === 0); // --- Debounced easing detection --- let easedConfirmed = false; const easingCandidate = (this.lastSpeed >= 2 && desiredSpeed > 0 && desiredSpeed < this.lastSpeed && dir === this.lastDir); if (easingCandidate) { if (this._easeCandidateSince === null) { this._easeCandidateSince = now; this._easeCandidateDir = dir; } else if ( this._easeCandidateDir === dir && (now - this._easeCandidateSince) >= this.easeConfirmMs ) { easedConfirmed = true; } } else { this._easeCandidateSince = null; this._easeCandidateDir = 0; } if (!this.fineMode && (directionChanged || hitNeutralFromIntent || easedConfirmed)) { this.fineMode = true; this._easeCandidateSince = null; if (speed === 0) return { dir: 0, speed: 0, note: "enter fine (neutral)" }; if (directionChanged) return { dir, speed: 1, note: "enter fine (turn)" }; if (easedConfirmed) return { dir, speed: 1, note: "enter fine (ease)" }; return { dir, speed: 1, note: "enter fine" }; } if (this.fineMode) { if (speed === 0) return { dir: 0, speed: 0, note: "fine mode" }; return { dir, speed: 1, note: "fine mode" }; } return { dir, speed, note: null }; } updateRaw(rawInPct) { if (typeof rawInPct !== "number") return null; if (this.actualOutPct === null) this.actualOutPct = this.minOutPct; if (this.centerInPct === null) this.centerInPct = rawInPct; const base = this._baseIntent(rawInPct); const applied = this._applyFineMode(base); this.currentDir = applied.dir; this.currentSpeed = applied.speed; const note = applied.note || base.reason || null; const key = this.currentDir + "|" + this.currentSpeed + "|" + (this.fineMode ? "F" : "-") + "|" + (this.neutralLatched ? "N" : "-") + "|" + (note || ""); const intentChanged = (key !== this._lastIntentKey); this._lastIntentKey = key; this.lastDir = this.currentDir; this.lastSpeed = this.currentSpeed; return { intentChanged, rawInPct, dir: this.currentDir, speed: this.currentSpeed, fineMode: this.fineMode, note }; } _tick() { if (this.actualOutPct === null) return; if (this.currentDir === 0 || this.currentSpeed === 0) return; this.actualOutPct = clamp( this.actualOutPct + (this.currentDir * this.currentSpeed), this.minOutPct, this.maxOutPct ); } getActualOutPct() { return (this.actualOutPct === null) ? null : Math.round(this.actualOutPct); } stop() { if (this._timer) { clearInterval(this._timer); this._timer = null; } return this.getActualOutPct(); } } module.exports = RateDetentController;

      That can be tested with a main.js like this

      var buttons = require("buttons"); var flicapp = require("flicapp"); var RateDetentController = require("./rateDetentController"); var ROTATE_ARM_MS = 700; var CLICK_SUPPRESS_AFTER_ROTATE_MS = 800; var speakerVirtualDeviceId = null; var lightVirtualDeviceId = null; var blindVirtualDeviceId = null; var stateByKey = {}; var rotateSessionByBdaddr = {}; var suppressClicksUntilByBdaddr = {}; var ctrlByKey = {}; var lastFinalOutByDevKey = {}; function keyOf(obj) { return obj.bdaddr + ":" + obj.buttonNumber; } function sizeLabel(buttonNumber) { return buttonNumber === 0 ? "BIG" : "small"; } function clamp01(x) { return x < 0 ? 0 : (x > 1 ? 1 : x); } function toPct01(x01) { return typeof x01 === "number" ? Math.round(clamp01(x01) * 100) : null; } function pctTo01(pct) { return clamp01(pct / 100); } function isInRotateMode(bdaddr) { return !!rotateSessionByBdaddr[bdaddr]; } function dirText(d) { return d > 0 ? "UP" : (d < 0 ? "DOWN" : "NEUTRAL"); } function clearArmTimer(st) { if (st && st.armTimer) { clearTimeout(st.armTimer); st.armTimer = null; } } function startRotateMode(st) { if (!st || st.rotateArmed) return; st.rotateArmed = true; rotateSessionByBdaddr[st.bdaddr] = { key: st.key, sizeLabel: st.sizeLabel, startedPrinted: false }; } function inToOut(inPct, minOut, maxOut) { var range = maxOut - minOut; if (range <= 0) return minOut; return minOut + (inPct / 100) * range; } function outToIn(outPct, minOut, maxOut) { var range = maxOut - minOut; if (range <= 0) return 0; var v = ((outPct - minOut) / range) * 100; if (v < 0) v = 0; if (v > 100) v = 100; return Math.round(v); } function syncVirtualFromOut(type, id, outPct, minOut, maxOut) { var inPct = outToIn(outPct, minOut, maxOut); var v01 = pctTo01(inPct); if (type === "Speaker") flicapp.virtualDeviceUpdateState("Speaker", id, { volume: v01 }); else if (type === "Light") flicapp.virtualDeviceUpdateState("Light", id, { brightness: v01 }); else if (type === "Blind") flicapp.virtualDeviceUpdateState("Blind", id, { position: v01 }); } // ---- buttons ---- buttons.on("buttonDown", function (obj) { if (!obj) return; var k = keyOf(obj); stateByKey[k] = { key: k, bdaddr: obj.bdaddr, buttonNumber: obj.buttonNumber, sizeLabel: sizeLabel(obj.buttonNumber), rotateArmed: false, armTimer: setTimeout(function () { startRotateMode(stateByKey[k]); }, ROTATE_ARM_MS) }; }); buttons.on("buttonUp", function (obj) { if (!obj) return; var k = keyOf(obj); var st = stateByKey[k]; clearArmTimer(st); if (isInRotateMode(obj.bdaddr)) { delete rotateSessionByBdaddr[obj.bdaddr]; suppressClicksUntilByBdaddr[obj.bdaddr] = Date.now() + CLICK_SUPPRESS_AFTER_ROTATE_MS; var keys = Object.keys(ctrlByKey); for (var i = 0; i < keys.length; i++) { var entry = ctrlByKey[keys[i]]; if (!entry || entry.bdaddr !== obj.bdaddr) continue; var finalOut = entry.ctrl.stop(); if (finalOut !== null) { console.log(entry.label + " - rotate end - " + entry.prettyName + " - final out " + finalOut + "%"); lastFinalOutByDevKey[entry.devKey] = finalOut; syncVirtualFromOut(entry.type, entry.id, finalOut, entry.minOut, entry.maxOut); } delete ctrlByKey[keys[i]]; } } if (st && !st.rotateArmed) { if (obj.gesture === "up" || obj.gesture === "down" || obj.gesture === "left" || obj.gesture === "right") { console.log(st.sizeLabel + " - " + obj.gesture.toUpperCase()); } } delete stateByKey[k]; }); buttons.on("buttonSingleOrDoubleClick", function (obj) { if (!obj) return; if (isInRotateMode(obj.bdaddr)) return; var suppressUntil = suppressClicksUntilByBdaddr[obj.bdaddr] || 0; if (Date.now() < suppressUntil) return; var lbl = sizeLabel(obj.buttonNumber); console.log(lbl + (obj.isDoubleClick === true ? " - DOUBLECLICK" : " - CLICK")); }); // ---- rotate ---- flicapp.on("virtualDeviceUpdate", function (metaData, values) { if (!metaData || !metaData.dimmableType || !metaData.virtualDeviceId) return; var bdaddr = metaData.buttonId; if (!bdaddr) return; var session = rotateSessionByBdaddr[bdaddr]; if (!session) return; var type = metaData.dimmableType; var id = metaData.virtualDeviceId; if (type === "Speaker" && speakerVirtualDeviceId === null) speakerVirtualDeviceId = id; if (type === "Light" && lightVirtualDeviceId === null) lightVirtualDeviceId = id; if (type === "Blind" && blindVirtualDeviceId === null) blindVirtualDeviceId = id; if (type === "Speaker" && id !== speakerVirtualDeviceId) return; if (type === "Light" && id !== lightVirtualDeviceId) return; if (type === "Blind" && id !== blindVirtualDeviceId) return; var inPct = null; var prettyName = null; if (type === "Speaker") { inPct = values && typeof values.volume === "number" ? toPct01(values.volume) : null; prettyName = "tv speaker"; } else if (type === "Light") { inPct = values && typeof values.brightness === "number" ? toPct01(values.brightness) : null; prettyName = "living room lights"; } else if (type === "Blind") { inPct = values && typeof values.position === "number" ? toPct01(values.position) : null; prettyName = "test"; } if (inPct === null) return; var devKey = bdaddr + "|" + type + "|" + id; var entry = ctrlByKey[devKey]; var minOut = 0, maxOut = 100; if (type === "Speaker") { minOut = 0; maxOut = 80; } else if (type === "Light") { minOut = 5; maxOut = 100; } else if (type === "Blind") { minOut = 0; maxOut = 100; } if (!entry) { var storedOut = lastFinalOutByDevKey[devKey]; var startOut = (typeof storedOut === "number") ? storedOut : Math.round(inToOut(inPct, minOut, maxOut)); syncVirtualFromOut(type, id, startOut, minOut, maxOut); var label = session.sizeLabel; var ctrl = new RateDetentController({ initialOutPct: startOut, tickMs: 333, // STICKY NEUTRAL (tweak here) deadbandEnter: 5, deadbandExit: 9, // SENSITIVITY tier1MaxOff: 25, tier2MaxOff: 40, extremeBandPct: 10, minOutPct: minOut, maxOutPct: maxOut }); entry = { bdaddr: bdaddr, devKey: devKey, type: type, id: id, prettyName: prettyName, label: label, ctrl: ctrl, minOut: minOut, maxOut: maxOut, lastOut: null }; ctrlByKey[devKey] = entry; if (!session.startedPrinted) { session.startedPrinted = true; console.log(label + " - rotate mode - start " + prettyName + " - out " + startOut + "% (in " + outToIn(startOut, minOut, maxOut) + "%)"); } } var u = entry.ctrl.updateRaw(inPct); if (!u) return; var outNow = entry.ctrl.getActualOutPct(); // intent change line if (u.intentChanged) { console.log( entry.label + " - " + entry.prettyName + " - out " + (outNow === null ? "?" : outNow) + "% - " + dirText(u.dir) + " " + u.speed + " - in " + inPct + "%" + (u.note ? " - " + u.note : "") ); } // tick/output line (only if output changed) if (outNow !== null && outNow !== entry.lastOut) { entry.lastOut = outNow; console.log(entry.label + " - " + entry.prettyName + " - out " + outNow + "% - " + dirText(u.dir) + " " + u.speed); } }); console.log("Ready. Short press: click/double + gesture. Hold >= 700ms: rotate-only until release.");
    • chriscam85

      DUO Behaviors
      General Discussion • • chriscam85

      3
      0
      Votes
      3
      Posts
      95
      Views

      chriscam85

      @Emil said in DUO Behaviors:

      on. Which actions will be performed thus depend on what actions have been set up on that receiver, to be triggered when the controller in question is pressed. So if you have a Flic Button paired to both a phone and a hub, where you have for the phone assigned an action when the button is double clicked (only), but on the hub you have set up an action to be performed when the button is held (only), then no action will be performed upon holding the button down, if the button currently has an active connection to the phone.

      Ok, so there is no "fallback" concept. It's just based on where it's currently connected. I did a test setup for this, to confirm. When connected to the hub, it won't let you setup through pairing to the phone. So, in the hub disconnect the duo, then pair to the phone. Set up the duo. Deselect the "disconnected" on the hub version, and now they will both work, just whichever is connected takes precedence. Ex: Phone dies or turn off bluetooth, commands will go to hub now. Duo will stay connected to hub even when phone is restored bluetooth.

      So, is it possible to control the volume of my phone via Twist (since it's only allowed when hub-connected)?

      My questions around Flic Control and Change Config are around how to swap functionality of the same controller. So, maybe small button toggles between the controller being a volume controller vs a media controller (volume up, down, mute vs play/pause, next, prev, skip forward, skip backward, set sleep timer) note: I do understand gestures are supposed to help here, but I get really inconsistent results.

    • shahan

      OSC with Flic 2
      General Discussion • • shahan

      3
      0
      Votes
      3
      Posts
      67
      Views

      Emil

      Another way would be to use e.g. the flic lib without using the hub to listen to events from button presses, and then generate and send the OSC messages yourself.

    • soulbarn2

      Pairing with a group of existing HomeKit bulbs…
      General Discussion • • soulbarn2

      2
      0
      Votes
      2
      Posts
      54
      Views

      Emil

      @soulbarn2 Only the Flic Hub LR can be added to Homekit as a bridge accessory, exposing its added Flics. In this case you should, in Homekit, press each Govee device and share it (by obtaining a Matter passkey), then enter this passkey into the Flic app while connected to the Flic Hub in the Matter provider. That will pair the Flic Hub with the Govee light, so you can control it from the Flic Hub. Repeat this process for each light. There is unfortunately no way to share groupings of devices across different Matter fabrics.

    • gordon 0

      web page refresh ignores flic button
      General Discussion • • gordon 0

      2
      0
      Votes
      2
      Posts
      56
      Views

      gordon 0

      @gordon-0
      I found this: for any IOS browser Webkit, the browser distinguishes between a fresh load initiated by navigation and a technical refresh gesture. The latter requires an additional tap on the screen by the user before input fields can be focused for typing. This is a deliberate security measure by Apple to enhance user control and prevent a poor user experience.

    • A Former User

      Curious Potential Buyer
      General Discussion • • A Former User

      1
      0
      Votes
      1
      Posts
      27
      Views

      No one has replied