[HomeyScript] Timeline Wrangler

Timeline Wrangler

As the Community App Store has been offline for a while making the Timeline Manager² app unavailable, and seen several people that want to maintain their notification timeline on a occasional basis I’ve tried to make a HomeyScript that can do exactly that.

How to use:

The only thing you need is an Homey Pro (any version, besides Homey (Cloud)) and the app HomeyScript.

When you have that app installed you can go to the Web GUI => Scripts and click on “New HomeyScript” on the bottom-left of the page.
A new script will get created and you can give it a name (not mandatory, just easier to find it back later on), “Timeline Wrangler” is a good example :wink:
Then all you need to do is copy and paste the entire script below (118 lines) into the top field, and press the “Save” button.

How to activate the script:

You can activate this script with the “Test” button or from any flow however you want, just pre-specify the code’s settings in the top of the code to whatever you want and activate the script.

Alternatively you can also use the “Code” flowcard of HomeyScript in Advanced Flows, it will return a [string] what it has done.

Example Flow(s):

Changelog:

  • v1.4 (August 10, 2024):
    • Fixed: Fix for empty name categories
  • v1.3 (October 2, 2023):
    • Fixed: Fix for nameless categories
  • v1.2 (October 2, 2023):
    • Added: “Contains Text” filter
  • v1.1 (October 1, 2023):
    • Fixed: how hours are looked at
  • v1.0 (October 1, 2023):
    • Initial release

Disclaimer:

Use this script on your own discretion, if the “deleteNotifications” setting is true, it will delete the notifications it mentions.

Please do keep the thread clean, any non related replies will get deleted!
If you have (general) questions about HomeyScript or the used Web API, then this is not the thread for that.

The Script:

// Timeline Wrangler v1.4

// ==================== User Settings ====================
const deleteNotifications = false; // If "true" = actually removes notifications, if "false" = display only
const daysOld = 5; // Amount of days that need to have past before a notification is deemed old
const hoursOld = 0; // Amount of hours that need to have past before a notification is deemed old
const maximumNotifications = 1000; // The maximum amount of notifications that stay, anything more will get deleted
const containsText = ''; // Only deletes notifications that contains the specified text, Empty ( '' ) = not used
const showCategories = false; // If "true" it will show all timeline categories you have and had in the past
const useCategory = false; // If "true" it will only remove from the specified categories below

// Separate categories by comma ( , )
const categories = [
  'homey:manager:apps',
  'homey:manager:backup',
  'homey:manager:energy',
  'homey:manager:experiments',
  'homey:manager:flow',
  'homey:manager:presence',
  'homey:manager:safety',
  'homey:manager:updates',
  'homey:manager:users',
  'homey:manager:zigbee',
];

// ==================== Don't change anything below here ====================
const removedNotifications = [];
const notificationCategories = [];
let amountNotifications = 0;
let amountKept = 0;

if (showCategories) {
  await Homey.notifications.getOwners()
    .then(owners => {
      log(Object.keys(owners).length, 'Timeline Categorie(s):');
      Object.keys(owners).forEach(key => {
        if (owners[key].uriObj.hasOwnProperty('name') && owners[key].uriObj.name) {
          log('-', owners[key].uriObj.name, (owners[key].uriObj.name.length <= 5) ? '\t\t\t|' : (owners[key].uriObj.name.length <= 12) ? '\t\t|' : '\t|', '\'' + key + '\'');
        }
        else {
          log('- Unkown Name', '\t\t|', '\'' + key + '\'');
        }
      });
      log('\n');
    })
    .catch(err => {
      log(err);
      return 'Getting categories failed';
    });
}

await Homey.notifications.getNotifications()
  .then(notifications => {
    amountNotifications = Object.keys(notifications).length;
    log(amountNotifications, 'Timeline Notification(s):');

    Object.keys(notifications).forEach(key => {
      const currentDate = new Date();
      const dateCreated = new Date(notifications[key].dateCreated);
      const timeDifference = (currentDate - dateCreated) / 1000;
      const daysDifference = Math.floor(timeDifference / 86400);
      const hours = Math.floor((timeDifference % 86400) / 3600);
      const hoursDifference = Math.floor(timeDifference / 3600);
      const minutes = Math.floor((timeDifference % 3600) / 60);

      if (
        ((containsText.length > 0 && notifications[key].excerpt.toLowerCase().includes(containsText.toLowerCase()))
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else if (
        ((containsText.length === 0 && useCategory)
        && (categories.includes(notifications[key].ownerUri))
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else if (
        ((containsText.length === 0 && !useCategory)
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else {
        log('- Keeping: ', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');
        amountKept++;
      }
    });
  })
  .catch(err => {
    log(err);
    return 'Script failed';
  });

if (removedNotifications.length > 0) {
  return 'Removed ' + removedNotifications.length + ' out of ' + amountNotifications + ' notification(s).';
}

return 'No notifications were (actually) removed';
9 Likes

Hey that’s awesome, I was under the impression this was only possible in the web api playground.

(Pro 2019 v8.1.6)

One thingy, it shows my categories correctly, but I also got an error when I set:

const showCategories = true;

Although it runs fine when I set it to ‘false’ again.

❌ Script Error
⚠️ TypeError: Cannot read properties of undefined (reading 'length')
    at Timeline_Wrangler.js:44:68
    at Array.forEach (<anonymous>)
    at Timeline_Wrangler.js:43:27
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Timeline_Wrangler.js:40:3
    at async HomeyScriptApp.runScript (/app.js:495:22)
    at async Object.runScript (/api.js:30:22)

.

Great! I was about to request it :stuck_out_tongue_winking_eye: :heart_eyes:

Something must have changed in the later updates then, as there was a SDKv3 update in v10 of the older Pro’s. :thinking: what are the rest of your settings?
Or do you perhaps have one of those weird nameless categories if you go to the settings of notifications?

EDIT: That probably is the issue, could you try v1.3? There it should be accounted for any nameless categories.

1 Like

Oh yes, I indeed have one nameless category!
No clue how it got there.

:white_check_mark: Yup, the error is gone with v1.3, thanks!

AND this is neat, I now know from the script results what the nameless category is from:

- Unkown Name 		| 'homey:app:nl.onzewifi.timelinemanager2'

(it can’t be a coincidence it’s from the Timeline manager ² app :face_with_hand_over_mouth: )

This is also good news for everyone with the infamous nameless categories on the Pro 2023 to find out what those are from.

Some flow ideas if you like.

Auto remove certain ‘Low Battery’ notifications by Homey, most of the time annoying and useless when a device uses rechargable batteries, like robo mowers, and robo vacs

  • This flow is triggered by new timeline notifications,
  • If it matches the phrase you enter as argument to this Homeyscript card,
    Screenshot from 2023-10-27 03-18-53
    (find the used script below)
    • in this example: **My_Mower** is low on battery (an exact part of the original message in the Timeline notification),
  • Then, it removes the timeline notification right away
    .


  • Detail of how to enter the search phrase argument
    Screenshot from 2023-10-27 02-46-52

The slightly adjusted script:

    • the argument is used with line
      const containsText = args[0]; // Only deletes notifications that contains the specified text
    • The script returns the used argument along with the ‘flows removed’ message
    • Note: the categories list is my own, but it is not used here
show me the script
// DEL timeline msgs ALL matching | Argument = searchphrase - Timeline_Wrangler v1.3p

// ==================== User Settings ====================
const deleteNotifications = true; // If "true": actually removes notifications, if "false": display only
const daysOld = 0; // Amount of days that need to have past before a notification is deemed old
const hoursOld = 0; // Amount of hours that need to have past before a notification is deemed old
const maximumNotifications = 250; // The maximum amount of notifications that stay, anything more will get deleted
const containsText = args[0]; // Only deletes notifications that contains the specified text, Empty ( '' ) = not used
const showCategories = false; // If "true" it will show all timeline categories you have and had in the past
const useCategory = false; // If "true" it will only remove from the specified categories below

// Separate categories by comma ( , )
const categories = [
  'homey:manager:flow',
  'homey:manager:presence',
  'homey:manager:apps',
  'homey:manager:updates',
  'homey:manager:zigbee',
  'homey:manager:energy',
  'homey:manager:backup',
  'homey:manager:users',
  'homey:app:com.athom.flowchecker',
  'homey:manager:security',
  'homey:app:nl.qluster-it.DeviceCapabilities',
  'homey:app:net.i-dev.betterlogic',
  'homey:app:com.spkes.telegramNotifications',
  'homey:manager:safety',
  'homey:app:nl.nielsdeklerk.log',
  'homey:manager:experiments',
];

// ==================== Don't change anything below here ====================
const removedNotifications = [];
const notificationCategories = [];
let amountNotifications = 0;
let amountKept = 0;

if (showCategories) {
  await Homey.notifications.getOwners()
    .then(owners => {
      log(Object.keys(owners).length, 'Timeline Categorie(s):');
      Object.keys(owners).forEach(key => {
        if (owners[key].uriObj.hasOwnProperty('name')) {
          log('-', owners[key].uriObj.name, (owners[key].uriObj.name.length <= 5) ? '\t\t\t|' : (owners[key].uriObj.name.length <= 12) ? '\t\t|' : '\t|', '\'' + key + '\'');
        }
        else {
          log('- Unkown Name', '\t\t|', '\'' + key + '\'');
        }
      });
      log('\n');
    })
    .catch(err => {
      log(err);
      return 'Getting categories failed';
    });
}

await Homey.notifications.getNotifications()
  .then(notifications => {
    amountNotifications = Object.keys(notifications).length;
    log(amountNotifications, 'Timeline Notification(s):');

    Object.keys(notifications).forEach(key => {
      const currentDate = new Date();
      const dateCreated = new Date(notifications[key].dateCreated);
      const timeDifference = (currentDate - dateCreated) / 1000;
      const daysDifference = Math.floor(timeDifference / 86400);
      const hours = Math.floor((timeDifference % 86400) / 3600);
      const hoursDifference = Math.floor(timeDifference / 3600);
      const minutes = Math.floor((timeDifference % 3600) / 60);

      if (
        ((containsText.length > 0 && notifications[key].excerpt.toLowerCase().includes(containsText.toLowerCase()))
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else if (
        ((containsText.length === 0 && useCategory)
        && (categories.includes(notifications[key].ownerUri))
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else if (
        ((containsText.length === 0 && !useCategory)
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else {
        log('- Keeping: ', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');
        amountKept++;
      }
    });
  })
  .catch(err => {
    log(err);
    return 'Script failed';
  });

if (removedNotifications.length > 0) {
  return 'Removed ' + removedNotifications.length + ' out of ' + amountNotifications + ' notification(s).\nUsed Argument: ' + args[0];
}

return 'No notifications were (actually) removed. \nUsed Argument: ' + args[0];

Not sure if it is ‘too much’, but I’d like to share my interactive timeline cleanup, which uses this Telegram app, to be able to select pre-defined phrases as answer, or by entering a phrase to match certain timeline entries.
The timeline entries which contain the answer text , are to be deleted.

  • Impression of the Telegram question + answer system, via app settings:

  • The flow asks a question via Telegram, and it comes with selectable answers
    Screenshot from 2023-10-30 19-35-29

  • And the same for a free-to-answer question:
    Screenshot from 2023-10-30 19-41-43

  • Feedback
    Screenshot from 2023-10-30 19-36-29


  • Code used in the Homeyscript flow cards:
Click me
// DEL timeline msgs ALL matching | Argument = searchphrase - Timeline_Wrangler v1.3p

// ==================== User Settings ====================
const deleteNotifications = true; // If "true": actually removes notifications, if "false": display only
const daysOld = 0; // Amount of days that need to have past before a notification is deemed old
const hoursOld = 0; // Amount of hours that need to have past before a notification is deemed old
const maximumNotifications = 250; // The maximum amount of notifications that stay, anything more will get deleted
const containsText = args[0]; // Only deletes notifications that contains the specified text, Empty ( '' ) = not used
const showCategories = false; // If "true" it will show all timeline categories you have and had in the past
const useCategory = true; // If "true" it will only remove from the specified categories below

// Separate categories by comma ( , )
const categories = [
  'homey:manager:flow',
  'homey:manager:presence',
  'homey:manager:apps',
  'homey:manager:updates',
  'homey:manager:zigbee',
  'homey:manager:energy',
  'homey:manager:backup',
  'homey:manager:users',
  'homey:app:com.athom.flowchecker',
  'homey:manager:security',
  'homey:app:nl.qluster-it.DeviceCapabilities',
  'homey:app:net.i-dev.betterlogic',
  'homey:app:com.spkes.telegramNotifications',
  'homey:manager:safety',
  'homey:app:nl.nielsdeklerk.log',
  'homey:manager:experiments',
];

// ==================== Don't change anything below here ====================
const removedNotifications = [];
const notificationCategories = [];
let amountNotifications = 0;
let amountKept = 0;

if (showCategories) {
  await Homey.notifications.getOwners()
    .then(owners => {
      log(Object.keys(owners).length, 'Timeline Categorie(s):');
      Object.keys(owners).forEach(key => {
        if (owners[key].uriObj.hasOwnProperty('name')) {
          log('-', owners[key].uriObj.name, (owners[key].uriObj.name.length <= 5) ? '\t\t\t|' : (owners[key].uriObj.name.length <= 12) ? '\t\t|' : '\t|', '\'' + key + '\'');
        }
        else {
          log('- Unkown Name', '\t\t|', '\'' + key + '\'');
        }
      });
      log('\n');
    })
    .catch(err => {
      log(err);
      return 'Getting categories failed';
    });
}

await Homey.notifications.getNotifications()
  .then(notifications => {
    amountNotifications = Object.keys(notifications).length;
    log(amountNotifications, 'Timeline Notification(s):');

    Object.keys(notifications).forEach(key => {
      const currentDate = new Date();
      const dateCreated = new Date(notifications[key].dateCreated);
      const timeDifference = (currentDate - dateCreated) / 1000;
      const daysDifference = Math.floor(timeDifference / 86400);
      const hours = Math.floor((timeDifference % 86400) / 3600);
      const hoursDifference = Math.floor(timeDifference / 3600);
      const minutes = Math.floor((timeDifference % 3600) / 60);

      if (
        ((containsText.length > 0 && notifications[key].excerpt.toLowerCase().includes(containsText.toLowerCase()))
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else if (
        ((containsText.length === 0 && useCategory)
        && (categories.includes(notifications[key].ownerUri))
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else if (
        ((containsText.length === 0 && !useCategory)
        && (daysDifference >= daysOld && hoursDifference >= hoursOld))
        || maximumNotifications < amountKept
      ) {
        log('- Removing:', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');

        if (deleteNotifications) {
          Homey.notifications.deleteNotification({id: notifications[key].id});
          removedNotifications.push(notifications[key].id);
        }
      }
      else {
        log('- Keeping: ', notifications[key].id, '  |', notifications[key].ownerUri, '|', notifications[key].excerpt, '(' + notifications[key].dateCreated + ' - ' + daysDifference + ' day(s), ' + hours + ' hour(s) and ' + minutes + ' minute(s) old)');
        amountKept++;
      }
    });
  })
  .catch(err => {
    log(err);
    return 'Script failed';
  });

if (removedNotifications.length > 0) {
  return 'Removed ' + removedNotifications.length + ' out of ' + amountNotifications + ' notification(s).\nUsed Argument: ' + args[0];
}

return 'No notifications were (actually) removed. \nUsed Argument: ' + args[0];

But why do you use two Telegram bots? :smiley:

This seems to occur when notifications are created by apps that have spaces in their names like “MQTT Broker”, “Device Capabilities” and as you found out also “< Timeline Manager² >”.
When a notification is create from one of those apps, the category isn’t created correctly;

{
	"homey:app:nl.onzewifi.timelinemanager2": {
		"enabled": true,
		"push": false,
		"name": null,
		"size": 0,
		"uriObj": {
			"name": null
		}
	}
}

As far as I know the developers can’t do anything about it, it’s something in the API. These categories aren’t showing in the list of available categories when you try to clean the timeline from the Homey app (well at least from the 10.3.0-rc.12 firmware, but possible with other versions too).

I’ve updated the HCS version of < Timeline Manager² > to work around this problem.

I got this error by setting the variable ‘showCategories’ to true.

TypeError: Cannot read properties of null (reading 'length')
    at t-c.js:38:70
    at Array.forEach (<anonymous>)
    at t-c.js:36:27
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async t-c.js:33:3
    at async HomeyScriptApp.runScript (/app/app.js:364:22)
    at async Object.runScript (/app/api.js:30:22)

It was because the name of ‘homey:manager:google-assistant’ was null

I did fix it by changing “owners[key].uriObj.hasOwnProperty(‘name’)” on line 37 to ‘owners[key].uriObj.name’

Hmm, an empty name, curious how that managed to happen at all.
But your way will also stop working if there is no “name” tag what the current check does, but i’ll fix it in the code.
Thank you for noticing.

EDIT: Should be handled/fixed in v1.4.

1 Like