Homey script - is there a event listener ? Like on motion (edit- wrong title)

I see no Homey-Script channel, so I’ll dump my question here.

So, I want to create a script that starts with an object containing all settings for a script. Say:

const settings = {
hallway : { 
  multisensor : 'hallway multisensor',
  light: 'hallway light',
  time: 15
},
kitchen : { 
  multisensor : 'kitchen multisensor',
  light: 'kitchen light',
  time: 15
}
}

Now I want to iterate over the object to create one script to replace 10 flows (2 in example code would be more)

Is there a way to grab each instance (scene) and setup a replacement for a flow? So,

for each of settings
if multisensor detects movement
then turn on light
while scene is active
wait 15 minutes
then turn off light

Now, I’m not looking for someone to write it :slight_smile: It’s just I cannot find a way that’s understandable for me in the docs on how to run script on start and find code for a trigger listener (if 'kitchen mulitensor' motion == true) and dim ‘kitchen light’ to 10.

Does this makes sense? I’m not yet familiar with the Homey jargon :slight_smile:

HomeyScript scripts only run as part of a flow, not as a flow-replacement, so things like “if multisensor detects movement” and “wait 15 minutes” can’t be implemented using it.

Oh, that is not what I expected. That is a bummer to say the least. :slight_smile:

I really hate to create 10 flows for one reused logic. And than replace 12 cards each time to create the same logic for another scene. And then have to edit (and not forget one) 10 flows to make one simple logic change.

Personally I find flows chaotic, I would prefer to do things with code. Yes, yes, I know, a product like this is meant for a larger target group.

Are there such possibilities in apps? In other words, would a hacky work-around work if apps support event listeners?

An app would be able to do that, but if you need to read/control devices from other apps, you need to use homey-api: homey-api - npm

Perhaps my app Flow Event Bus might make things easier to implement using just flows.

1 Like

Can you explain a bit more what you like to achieve, cause there’s several apps which can make things easier / smarter (Device Capabilities, Zone Memory, Group etc) when creating flows.

Might be a stupid thought, but why didn’t you get Home Assistant instead of Homey? Or run it next to Homey, it’s truly a nice combo.

Thanks, well let’s start with the fundametals. I wish to create a flow or a script that can be used for multiple scenes. That when you find a flaw in your thought process you can change it once for each scene.

Each scene has:

  • one or more multisensors
  • one or more lights
  • custom time to the lights to stay on
  • custom dimlevel (different types of lamps, plus level 100 should be for switches without dim)
  • user should still be able to switch off lights manually (force of habit) So the script should have a motion ON debouncer. So that the motion sensor won’t turn it back on in, say, a minute while the person leaves the scene.

Currently all device types are of the same brand. But that might change in the future. And not all multisensors in the house should trigger a lamp in the same scene to turn on. Some are in sleeping rooms, just to check temperatures and humidity.

There are some extra factors to it, but I think these are the basics for me to learn and understand. And for me to expand from.

1 Like

:slight_smile: I’ve played with HA for a week. And I did like it. I jumped into the pre-order bandwagon with the idea for a system that has all the IO technologie on board and a lot of out-of-the-box predefined options. It felt more user friendly. And even though it might not do everything I wish I could do I was still willing to spend this amount of money for some ease. Even though I already have a stronger machine with only a z-wave antenna missing for HA. I must admit Homey does feel kind of claustrophobic for me, right now.

Yes, as written in our PMs, we can/will be trying AVDs for this first.

Perhaps in combination with

1 Like

I understand you’re alr busy helping with the flow construction?
Then I’ll shut up for now :crazy_face::wink::grimacing:

1 Like

Yes, @Arie_J_Godschalk has contacted me through PM with some additional help and context :slight_smile:

But as a note. I would still love to be able (in the future) to have a parallel option to code with the same functionality as flows. It feels like only the event/trigger listener is missing. All the other stuff is possible with HScript.

2 Likes

As mentioned this is not really possible, but you could build a HScript (HS) and then use 1 trigger per device,
if you put this in one advanced flow it should be pretty clean, the different triggers would/could all start the same script…
As far as output (actions) some apps/devices can be controlled through HS, AVD is a good example

Just my 2 cents, I am interested in what you come up with though :slight_smile:

I’ve thought about this. But I can’t find a way to pass the device name or scene name of the device that triggered the event. With strict naming conventions of devices you could turn on a light this way. So, 'Kitchen Multisensor'. You could strip the first word of the name and turn on a lamp with '${firstStrippedWord} Light'. The thing is that this would create a hacky workaround to get only a small part of the problem to work. Namely, Kitchen sensor turns on Kitchen light. This would ignore the stay on time per scene. And the dim settings per scene.

Well, come to think of it. You could store your settings object in this HS. Then you would only have to know the scene from which the device was triggered to grab the instance of the settings object with all the desired settings.

storing the settings separate is advisable. This makes the settings reusable. For example trigger a flow form the Multisensor Motion alert turns ON and again for Multisensor Motion alert turns OFF and maybe switch button is pressed when the user turns a light OFF manually.

// this settings object can also be saved to a seperate script to be able to reuse it.
const settings = {
  "Keuken": { // key named by scene name
    "enabled": true,
    "multisensor": [
      "keuken multisensor",
      "bijkeuken multisensor"
    ],
    "min_lux": 0,
    "max_lux": 200,
    "lights": [ // device ID's would be quicker, but human unreadable
      "keuken lamp",
      "bijkeuken lamp"
    ],
    "dim_min": 1,
    "dim_max": 10,
    "on_time": 10
  },
  "Gang Beneden": {
    "enabled": true,
    "multisensor": [
      "gang beneden multisensor"
    ],
    "min_lux": 0,
    "max_lux": 200,
    "lights": [
      "gang beneden lamp"
    ],
    "dim_min": 1,
    "dim_max": 15,
    "on_time": 5
  }
}

const scene = myArgs[0]; //"keuken"
const sceneSettings = settings[scene];

if (sceneSettings.enabled) {
  // now you can do all the actions with the custom settings.
  sceneSettings.lights.foreach(light => {
    // homey get all devices and iterate over them to get the right light device by name
    // then calculate and set the dim level to the desired setting
  })
}

–edit–
Hmm nope, this is not a good flexible approach. I think I cannot set the settings in one HS file and import them. Copy/pasting them to each script is defying the whole idea of coding.

How about using the below app as an inbetween?

You’d have (atleast) 2 items before the script, the first is the trigger and the 2nd the identifier.
Since its a temporary variable it will not clutter the system and you get the info you need

You can use Homeyscript as well instead of 'Temporary Variables, just what one preferres

Screenshot from 2023-07-03 16-32-27

Available cards
Screenshot from 2023-07-03 16-33-30
Screenshot from 2023-07-03 16-33-15

RIGHT! That’s my current approach as well.

It’s a WIP, the same script should handle alarm on and off.

The settings are in a separate script. This way I can re-use device relationships. I think I’ll put device relationships in a database later.

let settings = await Homey.apps.getApp({ id: 'com.athom.homeyscript' })
  .then(homeyScript => { return homeyScript.apiPost('script/11e33a58-bc05-4110-993e-da8759dec050/run'); })

if (settings) {
  settings = settings.returns;
  const scene = typeof myArgs !== 'undefined' ? myArgs[0] : "Woonkamer"; //"keuken"
  const sceneSettings = settings[scene];

  if (sceneSettings.enabled) {
    const devices = await Homey.devices.getDevices();

    _.forEach(sceneSettings.lights, lightName => {
      // oh dear, this gets messy with a growing amount of devices
      _.forEach(devices, device => {
        if (device.name === lightName) {
         //TODO; get current lux val fro multisensor  for the range map
          const dimLevel = sceneSettings.multisensors ? 
          map( 1,[sceneSettings.min_lux, sceneSettings.max_lux], [sceneSettings.dim_min, sceneSettings.dim_max]) : 100;
          device.setCapabilityValue('dim', dimLevel)
        }
      })
    })
  }
}

const map = (value, oldRange, newRange) => {
  var newValue = (value - oldRange[0]) * (newRange[1] - newRange[0]) / (oldRange[1] - oldRange[0]) + newRange[0];
  return Math.min(Math.max(newValue, newRange[0]), newRange[1]);
}

The problem with temp vars is, when you have 10 devices. You set 10 temp vars. Resulting in 10 unique tags in the argument input.
So I made one global var to set and use. I REALLY don’t like this. Feels messy and unreadable when coming back in half a year. I have to remember for the light to use I have used global vars, Homey script and Flows. One location for all the logic would still be preferable. But ay, it works :slight_smile:

– EDIT –

I think this is a “problem” in flows in general. If I have 10 devices that generate their own tags and they are connected to one card you het 10 tags (assuming the device creates only one tag) with the same name, but they are all unique. So I see, Luminance,Luminance,Luminance,Luminance,Luminance,Luminance,Luminance,Luminance,Luminance,Luminance But what Luminace is from the kitchen? Plus why not create one tag? So that the card that has 10 devices connected to it simply can use Luminance.

That depends on how you set it up, personally i’d have 1 or 2 vars.
e.g. the temp var would be “< name >|< extra info >” and the script would split that into 2 (or more vars) to keep it clean.

-=edit=-

Yes that’s true, but only if you have 10 luminance-es in your flow, I’m fairly sure this does not happen too often and if need be you could populate this data in HS directly through HS instead of using an argument.
I’m pretty sure arguments are meant for a few tags not 10…

So, I’m a few stages further now. This is my current approach. This is my first flow in combination with my first Homey Script. So any feedback is welcome!

This is the flow. I’m still bummed out that I can’t add trigger listeners to HS. Now I have to add 30 triggers when using 10 scenes. But ay, let’s move on.

_settings-motion-lights.js // still have to play with the lux settings and add device ids

return {
  "global": {
    "enabledID": "48726aca-1784-4328-9fd0-2ee6b9d33e4e" // virtual device to enable/disable global motion sensor use
  },
  "Keuken": {
    "enabled": true,
    "multisensors": [
      "9d4348de-c1c3-4341-a6e7-7f33c20b63dd",//"keuken multisensor",
      "319d966f-638e-44b0-9f76-7610ced72ee9",//"bijkeuken multisensor"
    ],
    "min_lux": 0,
    "max_lux": 200,
    "lights": [
      "94254ea1-4e87-4763-991d-6eedc52a36aa",//"keuken lamp",
      "433639e7-bc41-4a2a-a701-eb383da1ea19",//"bijkeuken lamp"
    ],
    "dim_min": 1,
    "dim_max": 10,
    "on_time": 1,
    "debouce_time": 1,
  },
  "Gang Beneden": {
    "enabled": true,
    "multisensors": [
      "gang beneden multisensor"// TODO: add device ID
    ],
    "min_lux": 0,
    "max_lux": 200,
    "lights": [
      "gang beneden lamp"// TODO: add device ID
    ],
    "dim_min": 1,
    "dim_max": 15,
    "on_time": 5,
    "debouce_time": 1,
  },
  "Gang Boven": {
    "enabled": true,
    "multisensors": [
      "gang boven multisensor voor",// TODO: add device ID
      "gang boven multisensor achter"// TODO: add device ID
    ],
    "min_lux": 0,
    "max_lux": 200,
    "lights": [
      "gang boven lamp"// TODO: add device ID
    ],
    "dim_min": 1,
    "dim_max": 15,
    "on_time": 5,
    "debouce_time": 1,
  },
  "Toilet": {
    "enabled": true,
    "multisensors": [
      "toilet multisensor"// TODO: add device ID
    ],
    "min_lux": 0,
    "max_lux": 300,
    "lights": [
      "gang boven lamp"// TODO: add device ID
    ],
    "dim_min": 1,
    "dim_max": 40,
    "on_time": 15,
    "debouce_time": 1,
  }
}

light-motion-sensors.js

// dev settings
const debug = true;
const exec = false;
const skipDebounce = false;

// process arguments
let arguments = JSON.parse(typeof args[0] !== 'undefined' ? args[0] : '{"scene":"Keuken","command":"on"}');
// let arguments = JSON.parse(typeof args[0] !== 'undefined' ? args[0] : '{"scene":"Keuken","command":"off"}');
// let arguments = JSON.parse(typeof args[0] !== 'undefined' ? args[0] : '{"scene":"Keuken","command":"off_by_user"}');

// There must be a better way to import a script, right? why no import or require?
let settings = await Homey.apps.getApp({ id: 'com.athom.homeyscript' })
  .then(homeyScript => { return homeyScript.apiPost('script/11e33a58-bc05-4110-993e-da8759dec050/run'); })
settings = settings.returns;

let sceneSettings = settings[arguments.scene];
sceneSettings.timestampKey = `user_light_off_timestamp_${arguments.scene.toLowerCase()}`
const globalSettings = settings['global'];
const motionsensortAreEnabled = true;//await getEnabledSetting(globalSettings);

const writeOnTimeLine = async (message) => {
  await Homey.flow.runFlowCardAction({
    uri: "homey:manager:notifications:create_notification",
    id: "homey:manager:notifications:create_notification",
    args: {
      text: JSON.stringify(message)
    },
  });
}

const map = (value, oldRange, newRange) => {
  var newValue = (value - oldRange[0]) * (newRange[1] - newRange[0]) / (oldRange[1] - oldRange[0]) + newRange[0];
  return Math.min(Math.max(newValue, newRange[0]), newRange[1]);
}

const getDeviceByID = async (deviceID) => {
  return await Homey.devices.getDevice({ $skipCache: false, id: deviceID });
}

const getCapability = async (device, capability) => {
  return await device.capabilitiesObj[capability].value;
}

const setCapability = async (device, capability, value) => {
  return await device.setCapabilityValue(capability, value).catch(this.error);
}

const getCapabilityByDeviceID = async (deviceID, capability) => {
  return await getCapability(await getDeviceByID(deviceID), capability)
}

const setCapabilityByDeviceID = async (deviceID, capability, value) => {
  return await setCapability(await getDeviceByID(deviceID), capability, value)
}

const getEnabledSetting = async (globalSettings) => {
  return await getCapabilityByDeviceID(globalSettings.enabledID, 'onoff')
}

const getDeviceLux = async (device) => {
  return await getCapability(device, 'measure_luminance');
}

const turnOn = async () => {
  if (settings && motionsensortAreEnabled && sceneSettings && sceneSettings.enabled) {

    const lastUserOffTimestamp = global.get(sceneSettings.timestampKey);
    const currentTimestamp = Date.now();
    const debounceTimestamp = (lastUserOffTimestamp + (sceneSettings.on_time * 1000 * 60));
    if (currentTimestamp > debounceTimestamp || skipDebounce) {

      let multisensors = [];
      for await (const multisensorID of sceneSettings.multisensors) {
        multisensors.push(await getDeviceByID(multisensorID))
      }

      const curLux = await getDeviceLux(multisensors[0]);
      const dimLevel = Math.round(map(
        curLux,
        [sceneSettings.min_lux, sceneSettings.max_lux],
        [sceneSettings.dim_min, sceneSettings.dim_max]
      ));

      for (const lightID of sceneSettings.lights) {
        const lightIsOn = await getCapabilityByDeviceID(lightID, 'onoff')
        
        if (!lightIsOn) {
          // if dim_min === 100 then the light is only on/off, so not dimmable
          if (sceneSettings.dim_min < 100) {
            if (debug) {
              const message = `Dim Light: ${arguments.scene}`;
              log(message)
              writeOnTimeLine(message)
            }
            if (exec) {
              setCapabilityByDeviceID(lightID, 'dim', dimLevel)
            }

          } else {
            if (debug) {
              const message = `Switch on Light: ${arguments.scene}`;
              log(message)
              writeOnTimeLine(message)
            }
            if (exec) {
              setCapabilityByDeviceID(lightID, 'onoff', true)
            }
          }
        }
      }
    } else {
      if (debug) {
        const message = `${arguments.scene} motionsensor was tried to turn on within debounce time. Skipping action.`;
        log(message)
        writeOnTimeLine(message)
      }
    }
  }
}

const turnOff = async () => {
  if (settings && motionsensortAreEnabled && sceneSettings && sceneSettings.enabled) {
    await wait(sceneSettings.time_on * 60);

    let allMotionAlarmsAreOff = true;
    for await (const multisensorID of sceneSettings.multisensors) {
      const motionAlarmIsOn = await getCapabilityByDeviceID(multisensorID, 'alarm_motion')
      if (motionAlarmIsOn) {
        allMotionAlarmsAreOff = false
      }
    }

    if (allMotionAlarmsAreOff) {
      for (const lightID of sceneSettings.lights) {
        if (exec) {
          setCapabilityByDeviceID(lightID, 'onoff', true)
        }
        if (debug) {
          const message = `${arguments.scene} motionsensor waited and turned off light.`;
          log(message)
          writeOnTimeLine(message)
        }
      }
    }
  }
}

const userOff = async () => {
  if (settings && sceneSettings && sceneSettings.enabled) {
    global.set(sceneSettings.timestampKey, Date.now());
  }
}

switch (arguments.command) {
  case 'on':
    turnOn();
    break;
  case 'off':
    turnOff();
    break;
  case 'off_by_user':
    userOff();
    break
}