Reading Afvalwijzer in JSscripting or Jython

Recognise the issue.
For me that was solved by changing the afval type.
e.g. in the feed from api.mijnafvalwijzer.nl for my gemeente the type was plastic instead of pmd.
So changing the processPickupdates line to processPickupdates(pickupdates, 'plastic', 'plastic- en metaalafval en drinkkartons', ${itemPrefix}_pmd); fixed updating plastics.

Just use the url you see in the log manually in a browser and you see the types as per your gemeente

@ArdKuijpers you use icons like <garbage_papier> which are not standard openhab-classics ones.
do you have nice icons for this to share/recommend?

I figured it would be something like that.
Not as simple as your case but i managed to extract the JSON from afvalkalender and found the right naming for the afval type :slight_smile:

So for anybody using Cyclus NV change the processPickupdates lines to:

    processPickupdates(pickupdates, 'doos-karton-papier',           'oud papier',                              `${itemPrefix}_papier`);
    processPickupdates(pickupdates, 'appel-gft',                    'groente-, fruit- en tuinafval',           `${itemPrefix}_gft`);
    processPickupdates(pickupdates, 'zak-grijs-rest',               'restafval',                               `${itemPrefix}_restafval`);
    processPickupdates(pickupdates, 'petfles-blik-drankpak_pmd',    'plastic- en metaalafval en drinkkartons', `${itemPrefix}_pmd`);  
    processPickupdates(pickupdates, 'kerstboom-zonder-kruis',       'kerstbomen',                              `${itemPrefix}_kerstbomen`);

I removed the ā€œklein chemisch afvalā€ and added ā€œkerstbomenā€

Do you use the rule in a file or in the UI? My guess is you used the UI, right?. The script in the OP is meant to be used as a file-based rule in ā€¦/automation/js/. If you use this in the UI, the rules.JSRule({...}); creates a new rule every time. So for the UI, please use the version below. It works on my system:

configuration: {}
triggers: []
conditions: []
actions:
  - inputs: {}
    id: "3"
    configuration:
      type: application/javascript;version=ECMAScript-2021
      script: >
        {
          const logger = log('afvalkalender');

          // const MijnAfvalWijzerConfig = {
          //     huisnummer: "41",
          //     toevoeging: '',
          //     postcode: "3825AL",
          //     apikey: "<voer-hier-de-apikey-in>"
          // };

          const AfvalkalenderConfig = {  
              huisnummer: "17",
              postcode: "5581BG",
              afvalkalenderUri: 'https://afvalkalender.waalre.nl'
          };

          //const CONFIG = MijnAfvalWijzerConfig;
          const CONFIG = AfvalkalenderConfig;
          CONFIG.itemPrefix = "AfvalKalender";

          const { postcode, huisnummer, toevoeging } = CONFIG;
          itemPrefix = CONFIG.itemPrefix ?? "AfvalData";

          class pickupDate {
              constructor(date, type) {
                  this.date = date;
                  this.type = type;
              }
          }

          function processPickupdates(dates, afvalType, afvalTypeDescription, item, now = undefined) {
              if (!pickupdates || pickupdates.length == 0)
                  return;
              const afvalTypeDates = dates.filter(d => d.type == afvalType).map(d => d.date);
              if (afvalTypeDates.length == 0)
                  return;
              if (now == undefined)
                  now = time.ZonedDateTime.now();
              logger.debug(`Ophaaldata verwerken voor ${afvalType}: ${afvalTypeDescription} en Item ${item} on ${now}`);
              if (items.getItem(item, true) == null) {
                  logger.warn(`Kan ophaaldata niet verwerken want Item ${item} bestaat niet`);
                  return;
              }
              const today = now.toLocalDate();
              const tomorrow = today.plusDays(1);
              let pickupDate = afvalTypeDates.find(d => !d.isBefore(today));
              if (pickupDate == undefined)
                  pickupDate = afvalTypeDates.at(-1);

              items.getItem(item).postUpdate(pickupDate.format(time.DateTimeFormatter.ISO_LOCAL_DATE));
              const currentTime = now.toLocalTime();
              if (today.isEqual(pickupDate) && currentTime.hour() <= 12) {
                  message = `Vandaag wordt het ${afvalTypeDescription} opgehaald.`;
                  logger.info(message);
                  actions.NotificationAction.sendBroadcastNotification(message);
              }
              else if (tomorrow.isEqual(pickupDate) && currentTime.hour() > 12) {
                  message = `Morgen wordt het ${afvalTypeDescription} opgehaald.`;
                  logger.info(message);
                  actions.NotificationAction.sendBroadcastNotification(message);
              }
          }

          function setNextDateAndType(dates, nextDateItem, nextTypeItem, now=undefined)
          {
              if (items.getItem(nextDateItem, true) == null) {
                  logger.warn(`Kan volgende ophaaldatum niet verwerken want Item ${nextDateItem} bestaat niet`);
                  return;
              }
              if (items.getItem(nextTypeItem, true) == null) {
                  logger.warn(`Kan volgende ophaaldatum niet verwerken want Item ${nextTypeItem} bestaat niet`);
                  return;
              }

              now = now ?? time.ZonedDateTime.now();
              const today = now.toLocalDate();
              const sortedDates = dates.sort((d1, d2) => d1.date.compareTo(d2.date));
              let nextDate = sortedDates.find(d => !d.date.isBefore(today));
              if (nextDate == undefined)
                  nextDate = sortedDates.at(-1);
              items.getItem(nextDateItem).postUpdate(nextDate.date.format(time.DateTimeFormatter.ISO_LOCAL_DATE));
              items.getItem(nextTypeItem).postUpdate(nextDate.type);    
          }

          function getJSONResponse(uri)
          {
              const result = actions.HTTP.sendHttpGetRequest(uri);
              if (!result)
              {
                  logger.warn(`Geen respons gekregen van ${uri}`);
                  return undefined;
              }
              try {
                  return JSON.parse(result);
              }
              catch (error)
              {
                  logger.warn(`Kon respons van ${uri} niet naar JSON omzetten: ${error}`);
                  return undefined;
              }
          }

          /** 
           * Gets pickup dates from different garbage collectors from mijnafvalwijzer.
           */
          function getPickupdatesMijnafvalwijzer(postcode, huisnummer, toevoeging, apikey) {
              const dateFormatter = time.DateTimeFormatter.ofPattern('yyyy-MM-dd');
              const today = time.ZonedDateTime.now().format(dateFormatter);
              const uri = `https://api.mijnafvalwijzer.nl/webservices/appsinput/?method=postcodecheck&street=&postcode=${postcode}&huisnummer=${huisnummer}&toevoeging=${toevoeging}&apikey=${apikey}&app_name=afvalwijzer&platform=phone&mobiletype=android&afvaldata=${today}&version=58&langs=nl`;
              logger.debug(`Afvalwijzer URI: ${uri}`); 
              try {
                  const response = getJSONResponse(uri);
                  const ophaaldagen = response.ophaaldagen;
                  if (ophaaldagen.response !== 'OK')
                  {
                      logger.warn(`Ongeldige respons bij het downloaden van ophaaldata van ${uri}: ${pickupdates.error}`);
                      return undefined;
                  }
                  pickupdates = [];
                  return ophaaldagen.data.map(d => new pickupDate(time.LocalDate.parse(d.date, dateFormatter), d.type));
              }
              catch (error)
              {
                  logger.warn(`Kon ophaaldata niet destileren uit respons van ${uri}: ${error}`);
                  return undefined;
              }    
          }

          /**
           * Gets pickup dates from different garbage collectors from afvalkalender
           */

          function getPickupdatesAfvalkalender(postcode, huisnummer, afvalkalenderUri) {
              const uriAddress=`${afvalkalenderUri}/rest/adressen/${postcode}-${huisnummer}`;
              const responseAddress = getJSONResponse(uriAddress);
              if (!responseAddress) {
                  logger.warn(`Kon adresgegevens niet downloaden van ${uriAddress}`);
                  return undefined;
              }
              const bagId = responseAddress[0].bagId;
              const uriPickupdates=`${afvalkalenderUri}/rest/adressen/${bagId}/afvalstromen`;
              const responsePickupdates = getJSONResponse(uriPickupdates);
              if (!responsePickupdates)
              {
                  logger.warn(`Kon ophaaldata niet downloaden van ${uriPickupdates}`);
                  return undefined;
              }
              pickupdates = [];
              return responsePickupdates
                  .filter(d => d.icon && d.ophaaldatum)
                  .map(d =>  new pickupDate(time.LocalDate.parse(d.ophaaldatum,time.DateTimeFormatter.ofPattern('yyyy-MM-dd')), d.icon === "rest" ? "restafval" : d.icon));
          }

          logger.info(`Afval ophaaldata ophalen voor ${postcode}-${huisnummer}${toevoeging}`);
          if ('afvalkalenderUri' in CONFIG && CONFIG.afvalkalenderUri)
              pickupdates = getPickupdatesAfvalkalender(postcode, huisnummer, CONFIG.afvalkalenderUri);
          else
              pickupdates = getPickupdatesMijnafvalwijzer(postcode, huisnummer, toevoeging, CONFIG.apikey);

          if (pickupdates && pickupdates.length != 0) {
            processPickupdates(pickupdates, 'papier',    'oud papier',                              `${itemPrefix}_papier`);
            processPickupdates(pickupdates, 'gft',       'groente-, fruit- en tuinafval',           `${itemPrefix}_gft`);
            processPickupdates(pickupdates, 'restafval', 'restafval',                               `${itemPrefix}_restafval`);
            processPickupdates(pickupdates, 'pmd',       'plastic- en metaalafval en drinkkartons', `${itemPrefix}_pmd`);
            processPickupdates(pickupdates, 'kca',       'klein chemisch afval',                    `${itemPrefix}_kca`);

            setNextDateAndType(pickupdates, `${itemPrefix}_volgendeDatum`, `${itemPrefix}_volgendeType`);
            logger.info(`Afvalophaaldata bijgewerkt voor ${postcode}-${huisnummer}${toevoeging}`);
          } else {
            logger.warn(`Kon ophaaldata niet downloaden voor ${postcode}-${huisnummer}${toevoeging}`);
          }
        }
    type: script.ScriptAction

Right, it is EMCASCript-2021. And you are probably right about the notifications. I use them to get notifications in my phone app. You can either remove this or substitute these with other notifications (e.g. telegram)

Some icons:
garbage_restafval garbage_gft garbage_kca garbage_papier garbage_pmd

And a widget:

component: oh-list-card
config:
  footer: '="Volgende: " + items.AfvalData_volgendeType.state
    +    (dayjs(items.AfvalData_volgendeDatum.state).diff(dayjs().startOf("day"),     "days")
    == 0 ? " vandaag" :
    dayjs(items.AfvalData_volgendeDatum.state).diff(dayjs().startOf("day"),     "days")
    == 1 ? " morgen" : " over " +
    dayjs(items.AfvalData_volgendeDatum.state).diff(dayjs().startOf("day"),     "days")
    + " dagen")'
  title: Afvalkalender
slots:
  default:
    - component: oh-label-item
      config:
        icon: oh:garbage_gft
        item: AfvalData_gft
        title: Groente, Fruit en Tuin
    - component: oh-label-item
      config:
        icon: oh:garbage_papier
        item: AfvalData_papier
        title: Papier
    - component: oh-label-item
      config:
        icon: oh:garbage_restafval
        item: AfvalData_restafval
        title: Restafval

Ah, good find. I didnā€™t know these were different for the various Afvalkalenders.

1 Like

I created a script in the UI. All seems to work fine now.
Manually runnig the script creates the rule which will trigger the script twice a day.
Running the rule executes the script but does NOT add another rule.
So that should be fine then right?

And i if move the script into a file ( most of my openhab config is file based) what should be the file extension? .js ?

File based rules should be located in you configuration folder in the subdirrectory ./automation/js so e.g. /openhab/conf/automation/js/. Files should use the extension .js. Then the rules are automatically created and updated when you change them.

1 Like

Executing this script in the UI gives me this. Can you see why?

2022-01-10 19:38:46.950 [ERROR] [b.automation.script.javascript.stack] - Failed to execute script:

org.graalvm.polyglot.PolyglotException: SyntaxError: <eval>:5:10 Expected ; but found :

  - inputs: {}

          ^

<eval>:9:14 Expected an operand but found >

      script: >

              ^

	at org.graalvm.polyglot.Context.eval(Context.java:379) ~[?:?]

	at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:458) ~[?:?]

	at com.oracle.truffle.js.scriptengine.GraalJSScriptEngine.eval(GraalJSScriptEngine.java:426) ~[?:?]

	at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264) ~[java.scripting:?]

	at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocable.eval(DelegatingScriptEngineWithInvocable.java:51) ~[?:?]

	at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable.eval(InvocationInterceptingScriptEngineWithInvocable.java:69) ~[?:?]

	at org.openhab.automation.jsscripting.internal.scriptengine.DelegatingScriptEngineWithInvocable.eval(DelegatingScriptEngineWithInvocable.java:51) ~[?:?]

	at org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable.eval(InvocationInterceptingScriptEngineWithInvocable.java:69) ~[?:?]

	at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.lambda$0(ScriptActionHandler.java:62) ~[?:?]

	at java.util.Optional.ifPresent(Optional.java:183) ~[?:?]

	at org.openhab.core.automation.module.script.internal.handler.ScriptActionHandler.execute(ScriptActionHandler.java:59) ~[?:?]

	at org.openhab.core.automation.internal.RuleEngineImpl.executeActions(RuleEngineImpl.java:1180) ~[?:?]

	at org.openhab.core.automation.internal.RuleEngineImpl.runNow(RuleEngineImpl.java:1032) ~[?:?]

	at org.openhab.core.automation.internal.RuleEngineImpl.runNow(RuleEngineImpl.java:1048) ~[?:?]

	at org.openhab.core.automation.rest.internal.RuleResource.runNow(RuleResource.java:327) ~[?:?]

	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]

	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]

	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]

	at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?]

	at org.apache.cxf.service.invoker.AbstractInvoker.performInvocation(AbstractInvoker.java:179) ~[bundleFile:3.4.5]

	at org.apache.cxf.service.invoker.AbstractInvoker.invoke(AbstractInvoker.java:96) ~[bundleFile:3.4.5]

	at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:201) ~[bundleFile:3.4.5]

	at org.apache.cxf.jaxrs.JAXRSInvoker.invoke(JAXRSInvoker.java:104) ~[bundleFile:3.4.5]

	at org.apache.cxf.interceptor.ServiceInvokerInterceptor$1.run(ServiceInvokerInterceptor.java:59) ~[bundleFile:3.4.5]

	at org.apache.cxf.interceptor.ServiceInvokerInterceptor.handleMessage(ServiceInvokerInterceptor.java:96) ~[bundleFile:3.4.5]

	at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:265) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:225) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:298) ~[bundleFile:3.4.5]

	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:217) ~[bundleFile:3.4.5]

	at javax.servlet.http.HttpServlet.service(HttpServlet.java:707) ~[bundleFile:3.1.0]

	at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:273) ~[bundleFile:3.4.5]

	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:550) ~[bundleFile:9.4.43.v20210629]

	at org.ops4j.pax.web.service.jetty.internal.HttpServiceServletHandler.doHandle(HttpServiceServletHandler.java:71) ~[bundleFile:?]

	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:602) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1434) ~[bundleFile:9.4.43.v20210629]

	at org.ops4j.pax.web.service.jetty.internal.HttpServiceContext.doHandle(HttpServiceContext.java:294) ~[bundleFile:?]

	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:501) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1349) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[bundleFile:9.4.43.v20210629]

	at org.ops4j.pax.web.service.jetty.internal.JettyServerHandlerCollection.handle(JettyServerHandlerCollection.java:82) ~[bundleFile:?]

	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.Server.handle(Server.java:516) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:388) ~[bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:633) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:380) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:386) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883) [bundleFile:9.4.43.v20210629]

	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034) [bundleFile:9.4.43.v20210629]

	at java.lang.Thread.run(Thread.java:829) [?:?]

2022-01-10 19:38:46.985 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'Afval_Cyclus' failed: org.graalvm.polyglot.PolyglotException: SyntaxError: <eval>:5:10 Expected ; but found :

  - inputs: null

          ^

<eval>:9:14 Expected an operand but found >

      script: >

              ^


Did you switch to the code view first and then pasted the code?

That way you can enter a rule together with the conditions. If you just want the rule itself paste only the script part between (the part between script: > and type: script.ScriptAction

Thanks for your help. Seems to be working now.
All i had to change was this line:
CONFIG.itemPrefix = ā€œAfvalKalenderā€;
to this
//CONFIG.itemPrefix = ā€œAfvalKalenderā€;

Otherwise i had to change the items naming from prefix : AfvalData to AfvalKalender.

Hmmmm, I canā€™t get this script to work, I keep getting this error:

2022-09-05 22:46:56.613 [INFO ] [rulesupport.loader.ScriptFileWatcher] - Loading script '/etc/openhab/automation/jsr223/Afval.js'

2022-09-05 22:46:57.841 [ERROR] [ipt.internal.ScriptEngineManagerImpl] - Error during evaluation of script 'file:/etc/openhab/automation/jsr223/Afval.js': /etc/openhab/automation/jsr223/Afval.js:48:1 Expected an operand but found const

All I did was copy the script, made a new file in automation/jsr233 (I donā€™t have any other folders under automation), pasted the script and configured the required fields, so not it looks like this:

/**
 * 
 * Dit script bevat een JSRule die afval ophaaldata kan lezen van diverse afvalkalenders. De afvalkalenders worden
 * twee maal per dag uitgelazen. 
 * 
 * Creƫer de volgende Items om met de afvalkalender te kunnen werken:
 * 
 *      Group gAfvalData "Afval ophaaldata" <calendar> (gTuin)
 *      DateTime AfvalData_gft             "Groente, Fruit en Tuin [%1$ta %1$td/%1$tm]"  <garbage_gft>        (gAfvalData)
 *      DateTime AfvalData_papier          "Papier [%1$ta %1$td/%1$tm]"                  <garbage_papier>     (gAfvalData)
 *      DateTime AfvalData_restafval       "Restafval [%1$ta %1$td/%1$tm]"               <garbage_restafval>  (gAfvalData)
 *      DateTime AfvalData_pmd             "Plastic [%1$ta %1$td/%1$tm]"                 <garbage_pmd>        (gAfvalData) 
 *      DateTime AfvalData_kca             "Chemisch [%1$ta %1$td/%1$tm]"                <garbage_kca>        (gAfvalData) 
 *      DateTime AfvalData_volgendeDatum   "Volgende afvalophaaldag [%1$ta %1$td/%1$tm]" <clock>              (gAfvalData)
 *      String   AfvalData_volgendeType    "Volgende afvalophaalsoort"                                        (gAfvalData)
 * 
 *  En pas daarna de onderstaande configuratie aan. 
 *  Voor MijnAfvalwijzer zijn de volgende gegevens nodig:
 *      postcode, huisnummer, toevoeging en apikey
 *  Voor Afvalkalender zijn de volgende gegevens nodig:
 *      postcode, huisnummer, afvalKalenderUri
 * 
 * Kies voor de Afvalkalender eventueel een afvalkalender uit de onderstaande lijst
 * 
 * Cyclus NV: https://afvalkalender.cyclusnv.nl
 * HVC: https://apps.hvcgroep.nl
 * Dar: https://afvalkalender.dar.nl
 * Afvalvrij / Circulus-Berkel: https://afvalkalender.circulus-berkel.nl
 * Meerlanden: https://afvalkalender.meerlanden.nl
 * Cure: https://afvalkalender.cure-afvalbeheer.nl
 * Avalex: https://www.avalex.nl
 * RMN: https://inzamelschema.rmn.nl
 * Venray: https://afvalkalender.venray.nl
 * Den Haag: https://huisvuilkalender.denhaag.nl
 * Berkelland: https://afvalkalender.gemeenteberkelland.nl
 * Alphen aan den Rijn: https://afvalkalender.alphenaandenrijn.nl
 * Waalre: https://afvalkalender.waalre.nl
 * ZRD: https://afvalkalender.zrd.nl
 * Spaarnelanden: https://afvalwijzer.spaarnelanden.nl
 * Montfoort: https://afvalkalender.montfoort.nl
 * GAD: https://inzamelkalender.gad.nl
 * Cranendonck: https://afvalkalender.cranendonck.nl
 * 
 * Deze lijst is overgenomen uit: https://www.gadget-freakz.com/domoticz-dzvents-getgarbagedates-script/
 * 
 */

 const logger = log('afvalkalender');

 // const MijnAfvalWijzerConfig = {
 //     huisnummer: "41",
 //     toevoeging: '',
 //     postcode: "3825AL",
 //     apikey: "<voer-hier-de-apikey-in>"
 // };
 
 const AfvalkalenderConfig = {  
     huisnummer: "5",
     postcode: "3769XW",
     afvalkalenderUri: 'https://inzamelschema.rmn.nl'
 };
 
 //const CONFIG = MijnAfvalWijzerConfig;
 const CONFIG = AfvalkalenderConfig;
 //CONFIG.itemPrefix = "MijnEigenAfvalItem";
 
 rules.JSRule({
     name: "Afval ophaaldata updaten",
     description: "Haal gegevens op uit de afvalwijzer en/of afvalkalender over ophalen van afval",
     triggers: [
         triggers.GenericCronTrigger('0 50 22 * * ?'),
         triggers.GenericCronTrigger('0 30 19 * * ?')
     ],
     execute: data => {
         const { postcode, huisnummer, toevoeging } = CONFIG;
         itemPrefix = CONFIG.itemPrefix ?? "AfvalData";
 
         logger.debug(`Afval ophaaldata ophalen voor ${postcode}-${huisnummer}${toevoeging}`);
         if ('afvalkalenderUri' in CONFIG && CONFIG.afvalkalenderUri)
             pickupdates = getPickupdatesAfvalkalender(postcode, huisnummer, CONFIG.afvalkalenderUri);
         else
             pickupdates = getPickupdatesMijnafvalwijzer(postcode, huisnummer, toevoeging, CONFIG.apikey);
         
             if (!pickupdates || pickupdates.length == 0) {
             logger.warn(`Kon ophaaldata niet downloaden voor ${postcode}-${huisnummer}${toevoeging}`);
             return;
         }
 
 
         processPickupdates(pickupdates, 'papier',    'oud papier',                              `${itemPrefix}_papier`);
         processPickupdates(pickupdates, 'gft',       'groente-, fruit- en tuinafval',           `${itemPrefix}_gft`);
         processPickupdates(pickupdates, 'restafval', 'restafval',                               `${itemPrefix}_restafval`);
         processPickupdates(pickupdates, 'pmd',       'plastic- en metaalafval en drinkkartons', `${itemPrefix}_pmd`);
         processPickupdates(pickupdates, 'kca',       'klein chemisch afval',                    `${itemPrefix}_kca`);
         
         setNextDateAndType(pickupdates, `${itemPrefix}_volgendeDatum`, `${itemPrefix}_volgendeType`);
     }
 });
 
 class pickupDate {
     constructor(date, type) {
         this.date = date;
         this.type = type;
     }
 }
 
 function processPickupdates(dates, afvalType, afvalTypeDescription, item, now = undefined) {
     if (!pickupdates || pickupdates.length == 0)
         return;
     const afvalTypeDates = dates.filter(d => d.type == afvalType).map(d => d.date);
     if (afvalTypeDates.length == 0)
         return;
     if (now == undefined)
         now = time.ZonedDateTime.now();
     logger.debug(`Ophaaldata verwerken voor ${afvalType}: ${afvalTypeDescription} en Item ${item} on ${now}`);
     if (items.getItem(item, true) == null) {
         logger.warn(`Kan ophaaldata niet verwerken want Item ${item} bestaat niet`);
         return;
     }
     const today = now.toLocalDate();
     const tomorrow = today.plusDays(1);
     let pickupDate = afvalTypeDates.find(d => !d.isBefore(today));
     if (pickupDate == undefined)
         pickupDate = afvalTypeDates.at(-1);
     
     items.getItem(item).postUpdate(pickupDate.format(time.DateTimeFormatter.ISO_LOCAL_DATE));
     const currentTime = now.toLocalTime();
     if (today.isEqual(pickupDate) && currentTime.hour() <= 12) {
         message = `Vandaag wordt het ${afvalTypeDescription} opgehaald.`;
         logger.info(message);
         actions.NotificationAction.sendBroadcastNotification(message);
     }
     else if (tomorrow.isEqual(pickupDate) && currentTime.hour() > 12) {
         message = `Morgen wordt het ${afvalTypeDescription} opgehaald.`;
         logger.info(message);
         actions.NotificationAction.sendBroadcastNotification(message);
     }
 }
 
 function setNextDateAndType(dates, nextDateItem, nextTypeItem, now=undefined)
 {
     if (items.getItem(nextDateItem, true) == null) {
         logger.warn(`Kan volgende ophaaldatum niet verwerken want Item ${nextDateItem} bestaat niet`);
         return;
     }
     if (items.getItem(nextTypeItem, true) == null) {
         logger.warn(`Kan volgende ophaaldatum niet verwerken want Item ${nextTypeItem} bestaat niet`);
         return;
     }
 
     now = now ?? time.ZonedDateTime.now();
     const today = now.toLocalDate();
     const sortedDates = dates.sort((d1, d2) => d1.date.compareTo(d2.date));
     let nextDate = sortedDates.find(d => !d.date.isBefore(today));
     if (nextDate == undefined)
         nextDate = sortedDates.at(-1);
     items.getItem(nextDateItem).postUpdate(nextDate.date.format(time.DateTimeFormatter.ISO_LOCAL_DATE));
     items.getItem(nextTypeItem).postUpdate(nextDate.type);    
 }
 
 function getJSONResponse(uri)
 {
     const result = actions.HTTP.sendHttpGetRequest(uri);
     if (!result)
     {
         logger.warn(`Geen respons gekregen van ${uri}`);
         return undefined;
     }
     try {
         return JSON.parse(result);
     }
     catch (error)
     {
         logger.warn(`Kon respons van ${uri} niet naar JSON omzetten: ${error}`);
         return undefined;
     }
 }
 
 /** 
  * Gets pickup dates from different garbage collectors from mijnafvalwijzer.
  */
 function getPickupdatesMijnafvalwijzer(postcode, huisnummer, toevoeging, apikey) {
     const dateFormatter = time.DateTimeFormatter.ofPattern('yyyy-MM-dd');
     const today = time.ZonedDateTime.now().format(dateFormatter);
     const uri = `https://api.mijnafvalwijzer.nl/webservices/appsinput/?method=postcodecheck&street=&postcode=${postcode}&huisnummer=${huisnummer}&toevoeging=${toevoeging}&apikey=${apikey}&app_name=afvalwijzer&platform=phone&mobiletype=android&afvaldata=${today}&version=58&langs=nl`;
     logger.debug(`Afvalwijzer URI: ${uri}`); 
     try {
         const response = getJSONResponse(uri);
         const ophaaldagen = response.ophaaldagen;
         if (ophaaldagen.response !== 'OK')
         {
             logger.warn(`Ongeldige respons bij het downloaden van ophaaldata van ${uri}: ${pickupdates.error}`);
             return undefined;
         }
         pickupdates = [];
         return ophaaldagen.data.map(d => new pickupDate(time.LocalDate.parse(d.date, dateFormatter), d.type));
     }
     catch (error)
     {
         logger.warn(`Kon ophaaldata niet destileren uit respons van ${uri}: ${error}`);
         return undefined;
     }    
 }
 
 /**
  * Gets pickup dates from different garbage collectors from afvalkalender
  */
 
 function getPickupdatesAfvalkalender(postcode, huisnummer, afvalkalenderUri) {
     const uriAddress=`${afvalkalenderUri}/rest/adressen/${postcode}-${huisnummer}`;
     const responseAddress = getJSONResponse(uriAddress);
     if (!responseAddress) {
         logger.warn(`Kon adresgegevens niet downloaden van ${uriAddress}`);
         return undefined;
     }
     const bagId = responseAddress[0].bagId;
     const uriPickupdates=`${afvalkalenderUri}/rest/adressen/${bagId}/afvalstromen`;
     const responsePickupdates = getJSONResponse(uriPickupdates);
     if (!responsePickupdates)
     {
         logger.warn(`Kon ophaaldata niet downloaden van ${uriPickupdates}`);
         return undefined;
     }
     pickupdates = [];
     return responsePickupdates
         .filter(d => d.icon && d.ophaaldatum)
         .map(d =>  new pickupDate(time.LocalDate.parse(d.ophaaldatum,time.DateTimeFormatter.ofPattern('yyyy-MM-dd')), d.icon === "rest" ? "restafval" : d.icon));
 }

Any thoughts? Would love to let this work :slight_smile:

Hi, Iā€™m exploring this function too, and before I found this thread I was trying to extract the data using the ICal binding. According to our HA friends, this should be an option, too.

However, it seems that the iCal binding is relying on the username/password approach, which does not seem to be replaceable with postal code/ house number.
2nd, the provided solution seems to be covering any garbage collection service in The Netherlands. I have no idea how many would be covered using the binding, as probably not all of them use ximmio.

Maybe someone with more technical background is able to determine if the binding is capable of extracting the data? Maybe a dedicated Bridge is required to deal with the extract?

Hi @fennepa, the script does not support ximmio yet, but it seems that they are using an api that is not too complicated and seems to be open to the public. I could try to add it based on this HA afvalkalender implementation.

1 Like

FYI, made this rule for textual rules DSL:

rule "Afval ophaaldata inlezen"
when 
	Time cron "0 30 0 ? * * *"
then
	var Afvalkalender = sendHttpGetRequest("https://afvalkalender.cyclusnv.nl/rest/adressen/(bagId)/afvalstromen")
	var GFTAfval = transform("JSONPATH", "$[?(@.title == 'Groente-, fruit- en tuinafval')].ophaaldatum", Afvalkalender)
	CyclusGFT.postUpdate(GFTAfval)
	var GrijzeBak = transform("JSONPATH", "$[?(@.title == 'Restafval')].ophaaldatum", Afvalkalender)
	CyclusRest.postUpdate(GrijzeBak)
	var PMDAfval = transform("JSONPATH", "$[?(@.title == 'Plastic, metaal en drankenkartons')].ophaaldatum", Afvalkalender)
	CyclusPMD.postUpdate(PMDAfval)
	var PapierBak = transform("JSONPATH", "$[?(@.title == 'Oud papier & karton.')].ophaaldatum", Afvalkalender)
	CyclusPapier.postUpdate(PapierBak)
	logInfo("Cyclus ophaaldata", "GFT: {}, Rest: {}, PMD: {}, Papier: {}", GFTAfval, GrijzeBak, PMDAfval, PapierBak)
end

I am in the Cyclus area. The required (bagId) in the url can be found using the following address in your web browser

${afvalkalenderUri}/rest/adressen/${postcode}-${huisnummer}

Where afvalkalender is the garbage provider to be used and postcode and huisnummer speak for themselves. So for cyclus would be:

https://afvalkalender.cyclusnv.nl/rest/adressen/1234AB-567

For postcode 1234AB house number 567.

Iā€™ve basically created String items to write the retrieved next collection date as a string (i.e. 2024-02-02).

Say I want to create an action today when tomorrow there is a pick up date, just simply compare if tomorrowā€™s date is mentioned as a pick up date in one of the items:

var tomorrow = now().plusDays(1).toString().split("T").get(0)
if (CyclusGFT.state == tomorrow) {
     Do something
}

Also in textual rules / Rules DSL.