imports:
  - module: core/sources/demo
    source: demo
  - module: core/devices/httpcamera
    device: httpCamera
  - module: core/devices/device
    device: device
  - module: core/devices/sensor
    device: sensor
  - module: core/devices/variable
    device: variable
  - module: core/devices/group
    device: group
  - module: proxiti
    source: astronomy
  - module: tempo
    source: tempo

sources:
  - freebox: {
    type: demo
  }
  - freeboxSemeria: {
      type: demo
  }
  - robonect: {
      type: demo
  }
  - robonect-weather: {
      type: demo
  }
  - robonect-command: {
      type: demo
  }
  - robonect-error: {
      type: demo
  }
  - ZStick: {
      type: demo
  }
  - myZibase: {
      type: demo
  }
  - myIPX800: {
      type: demo
  }
  - astronomy: {
      type: astronomy,
      location: "06030"
  }
  - ESPCommands : {
      type: demo
  }
  - ESPs: {
      type: demo
  }
  - tempo: { type: tempo }
  - voice: {
      type: demo
  }

devices:
  - comm: { type: variable, widget: "comm", name: "Communication"}
  - aquarium:
    - controle : { type: variable, widget: "multistate:AUTO,ON,OFF:primary,secondary,danger", tags: 'aquarium-page', name: "Commande de l'aquarium"} 
    #- pompes : { type: device, widget: toggle, tags: 'aquarium-page', source: ZStick, id: "14-37-2-currentValue", name: "Pompes aquarium" }
    - pompes : { type: device, widget: toggle, tags: 'aquarium-page', source: ESPCommands, id: "3c6ed4:0", transform: "1=>OFF,0=>ON,OFF=>1,ON=>0", name: "Pompes aquarium" }
    #- lampes : { type: device, widget: toggle, tags: 'aquarium-page', source: ZStick, id: "14-37-1-currentValue", name: "Lampes UV aquarium" }
    - lampes : { type: device, widget: toggle, tags: 'aquarium-page', source: ESPCommands, id: "3c6ed4:4", transform: "1=>OFF,0=>ON,OFF=>1,ON=>0", name: "Lampes UV aquarium" }
    - lampes_start : { type: variable, widget: text, tags: 'aquarium-page', name: "Heure allumage aquarium" }
    - lampes_end : { type: variable, widget: text, tags: 'aquarium', name: "Heure extinction aquarium" }
    - pompes_start : { type: variable, widget: text, tags: 'aquarium-page', name: "Heure démarrage pompes aquarium" }
    - pompes_end : { type: variable, widget: text, tags: 'aquarium', name: "Heure arrêt pompes aquarium" }

  - sunset: { type: device, persistence: "mongo:sunset:1200:aggregate", widget: text, tags: 'astronomy', source: astronomy, id: sunsetTime, name: "Coucher du soleil" }
  - sunset_plus_30mn: { type: variable, debug: true, widget: text, tags: 'astronomy', name: "Coucher du soleil + 30mn" }
  - sunrise: { type: device, widget: text, tags: 'astronomy', source: astronomy, id: sunriseTime, name: "Lever du soleil" }
  - zenith: { type: device, widget: text, tags: 'astronomy', source: astronomy, id: zenithTime, name: "Zénith" }
  - daynight: { type: variable, widget: text, tags: 'astronomy', name: "Jour/nuit" }

  #- camera_interieure: { type: httpCamera, widget: camera, tags: 'aquarium-page', video-url: /assets/imgs/demo_camera_interieure.jpg, snapshot-url: /assets/imgs/demo_camera_interieure.jpg, name: "Caméra intérieure" }
  - camera_interieure: { type: httpCamera, widget: camera, tags: 'aquarium-page', video-url: /assets/imgs/demo_camera_interieure.jpg, snapshot-url: /assets/imgs/demo_camera_interieure.jpg, name: "Caméra intérieure" }
  #- camera_exterieure: { type: httpCamera, widget: camera, tags: 'portails, entrée', video-url: /assets/imgs/demo_camera_exterieure.jpg, snapshot-url: /assets/imgs/demo_camera_exterieure.jpg, name: "Caméra extérieure" }
  #- camera_exterieure: { type: httpCamera, widget: camera, tags: 'portails, entrée', video-url: /assets/imgs/demo_camera_exterieure.jpg, snapshot-url: /assets/imgs/demo_camera_exterieure.jpg, name: "Caméra extérieure" }
  - camera_exterieure: { type: httpCamera, widget: camera, tags: 'portails, entrée', video-url: /assets/imgs/demo_camera_exterieure.jpg, snapshot-url: /assets/imgs/demo_camera_exterieure.jpg, name: "Caméra extérieure" }
  - camera_piscine: { type: httpCamera, widget: camera, tags: 'piscine-page', video-url: /assets/imgs/demo_camera_piscine.jpg, snapshot-url: /assets/imgs/demo_camera_piscine.jpg, name: "Caméra piscine" }
  - alarme:
    - en_route: { type: device , widget: toggle, tags: 'alarme', source: myZibase, id: A1, name: "Alarme en route" }
    - presence : { type: variable, widget: text, tags: 'alarme', name: "Présence dans la maison"} 
    - sirene_interieure: { type: variable, widget: toggle, tags: 'alarme', source: myIPX800, id: 1, name: "Sirène intérieure" }
    - sirene_exterieure: { type: variable, widget: toggle, tags: 'alarme', source: myIPX800, id: 2, name: "Sirène extérieure" }
    - sensors: { type: group, widget: walkingman, tags: 'alarme', taglist: 'sensor-alarm', function:  !!js/function 'function (newValues, callback) {
            return callback(null, newValues.filter(v => v=="1").length > 0?"ON":"OFF");
          }', name: 'Détecteurs surveillés'}
    - presence-timeout : { type: variable, widget: text, tags: '', name: "Timeout présence (mn)"} 
    - fin-presence : { type: variable, widget: text, tags: 'alarme', name: "Heure de fin de présence"} 
  - garages:
    - porte: { type: sensor, widget: walkingman, tags: 'détecteurs, sensor-alarm', source: myIPX800, id: "INPUT5", name: "Porte garage du bas ou jardin", transform: "0=>OFF,1=>ON" }

  - bureau: { type: sensor, widget: walkingman, tags: 'détecteurs, sensor-alarm', source: myIPX800, id: "INPUT2", name: "Détecteur bureau", transform: "0=>OFF,1=>ON" }
  - salle_a_manger: { type: sensor, widget: walkingman, tags: 'détecteurs', source: myIPX800, id: "INPUT6", name: "Détecteur salle à manger", transform: "1=>OFF,0=>ON", camera: camera_interieure }
  - hall: { type: sensor, widget: walkingman, tags: 'détecteurs, sensor-timer, sensor-alarm', source: myIPX800, id: "INPUT3", name: "Détecteur hall", transform: "0=>OFF,1=>ON" }
  - escalier: { type: sensor, widget: walkingman, tags: 'détecteurs, sensor-timer, sensor-alarm', source: myIPX800, id: "INPUT4", name: "Détecteur escalier", transform: "0=>OFF,1=>ON" }
  - cuisine: { type: sensor, widget: walkingman, tags: 'détecteurs, sensor-alarm', source: myIPX800, id: "INPUT1", name: "Détecteur cuisine", transform: "0=>OFF,1=>ON" }

  - prise_camera: { type: device, widget: toggle, source: myZibase, id: "G2", name: "Prise Caméra" }




#rooms:
#  - cuisine:
#      name: Cuisine
#  - hall:
#      name: Hall

#zones:

#scenes:
#  - init:
#    - { actions: stairLightTimer, attribute: activated, value: true }
#    - { actions: hallLightTimer, attribute: activated, value: true }
#    - { actions: kitchenLightTimer, attribute: activated, value: true }
#  - normal:
#    - minuterie-hall:
#      - { actions: hallLightTimer, attribute: duration, value: 10:00 }
#    - aquarium:
#      - { actions: aquariumTimer, attribute: activated, value: true }
#  - invitation:
#    - minuterie-hall:
#      - { actions: hallLightTimer, attribute: duration, value: 15:00 }
#    - aquarium:
#      - { actions: aquariumTimer, attribute: activated, value: false }
#      - { device: lampes_aquarium, attribute: state, value: 'ON' }
#      - { device: pompes_aquarium, attribute: state, value: 'ON' }


  - semeria:
    - presence-timeline : { type: variable, debug: false, widget: vis-graph, tags: 'semeria', name: "Présence Séméria"} 
    - voix : { type: variable, widget: toggle, tags: "semeria voix", name: "Voix Séméria" }
    - nb_connected: { type: variable, widget: text, tags: 'semeria', name: "nb Connectés Séméria" }
    - hosts-connected: { type: sensor, widget: text, tags: 'semeria', source: freeboxSemeria, id: '/lan/browser/pub/', transform: !!js/function 'function (value) { 
      if (!value) return value; 
      //console.log("<<<<<",JSON.parse(value).length,JSON.parse(value)[0],">>>>>"); 
      let tab = JSON.parse(value).filter(lh => lh.active && lh.reachable && lh.vendor_name != "Freebox SAS");
      //console.log("<<<<<",tab.length,tab.length && tab[0],">>>>>"); 
      return tab.map(lh => lh.primary_name).join(", ");
    }', 
      persistence: "mongo:hosts_connected_semeria:0:raw",
      name: "Connectés Séméria" }
        #id:TTL:raw|aggregate:KEEP    
        # TTL: 0=default, 1h
        # KEEP: in minutes. default = 5 years (not implemented)
    - presence : { type: variable, widget: text, tags: 'semeria', name: "Présence au Séméria"} 
    - presence-couleur : { type: variable, widget: color, tags: 'semeria', name: "Couleur icon"} 
    - presence-message : { type: variable, widget: text, tags: 'semeria', name: "Message présence"} 
    - presence-last-changed-date : { type: variable, widget: text, tags: 'semeria', name: "Date de changement de présence"} 
    - connected-last-changed-date : { type: variable, widget: text, tags: 'semeria', name: "Date de changement d'objets connectés"} 
  - home:
    - dernier-appel: { type: sensor, widget: text, tags: 'téléphone-page', source: freebox, id: '/call/log/', transform: !!js/function 'function (value) { 
      if (!value) return value; 
      //console.log("<<<<<",JSON.parse(value).length,JSON.parse(value)[0],">>>>>"); 
      let tab = JSON.parse(value);
      //console.log("<<<<<",tab.length,tab.length && tab[0],">>>>>"); 
      if (tab.length>0)
        return tab[0].number==""?"Inconnu":tab[0].number;
      else
        return "";
    }', name: "Dernier appel" }
    - date-dernier-appel: { type: sensor, widget: text, tags: 'téléphone-page', source: freebox, id: '/call/log/', transform: !!js/function 'function (value) { 
      if (!value) return value; 
      //console.log("<<<<<",JSON.parse(value).length,JSON.parse(value)[0],">>>>>"); 
      let tab = JSON.parse(value);
      //console.log("<<<<<",tab.length,tab.length && tab[0],">>>>>"); 
      if (tab.length>0)
        return new Date(tab[0].datetime * 1000);
      else
        return "";
    }', name: "Date dernier appel" }
    - caracteristiques-dernier-appel: { type: variable, widget: text, tags: 'téléphone', name: "Appel:danger:nom:provenance"}
    - message-dernier-appel : { type: variable, widget: text, tags: 'téléphone', name: "Message dernier appel"} 
    - appel-en-cours: { type: sensor, widget: text, tags: 'téléphone-page', source: freebox, id: '/call/log/', transform: !!js/function 'function (value) { 
      if (!value) return value; 
      //console.log("<<<<<",JSON.parse(value).length,JSON.parse(value)[0],">>>>>"); 
      let tab = JSON.parse(value);
      //console.log("<<<<<",tab.length,tab.length && tab[0],">>>>>"); 
      if (tab.length>0 && tab[0].type=="missed" && tab[0].duration == 0) {
        if (tab[0].number=="") return "Inconnu";
        if (tab[0].contact_id != 0) {
          // handle contact
        }
        return tab[0].number;
      } else
        return "";
    }', name: "Appel en cours" }
    - couleur-telephone: { type: variable, widget: color, tags: 'téléphone', name: "Couleur du téléphone" }

  - lampes:
    - lampe_chambre_parents: { type: variable, widget: toggle, tags: 'lampes-presence', id: "ZA4", name: "Lampe Chambre parents" }
    - lampe_table_cuisine: { type: variable, widget: toggle, tags: 'lampes-presence', id: "ZP4", name: "Spots Table Cuisine" }
    - lampe_preau: { type: device, widget: toggle, tags: 'lampes-presence entrée', source: ZStick, id: "15-38-0-currentValue", name: "Lampe Préau" }
    - lampe_hall: { type: variable, widget: toggle, tags: 'lampes-presence', id: "ZC9", name: "Spots hall" }
    - lampe_rouge_haut: { type: device, widget: toggle, tags: 'lampes', source: ZStick, id: "17-37-2-currentValue", name: "Lampe rouge haut" }
    - lampe_rouge_bas: { type: device, widget: toggle, tags: 'lampes', source: ZStick, id: "17-37-1-currentValue", name: "Lampe rouge bas" }
    - lampe_escalier: { type: device, widget: toggle, tags: 'lampes-presence', source: ZStick, id: "13-38-0-currentValue", name: "Lampe escalier" }
    - nb_lampes: { type: group, widget: text, tags: 'lampes', function: count, taglist: 'lampes-presence', name: "Nombre de lampes" }
    - nb_lampes_ON: { type: group, widget: text, tags: 'lampes', function: !!js/function 'function (newValues, callback) {
          return callback(null, newValues.filter(v => v=="ON").length);
        }', taglist: 'lampes-presence', name: "Nombre de lampes allumées" }
    - couleur : { type: variable, widget: color, tags: 'lampes-couleur', name: "Couleur du widget de lampes"} 

  - piscine:
     - controle : { type: variable, widget: "multistate:AUTO,ON,OFF:primary,secondary,danger", tags: 'piscine-page', name: "Programmation filtration"} 
     - filtration: { type: device, debug: false, widget: toggle, tags: 'piscine-page', source: ESPCommands, id: "04a87b:5", 
        transform: "0=>OFF,1=>ON,OFF=>0,ON=>1",
        persistence: "mongo:piscine_ON_OFF:0:raw",
        name: "Filtration piscine" }
        #id:TTL:raw|aggregate:KEEP    
        # TTL: 0=default, 1h
        # KEEP: in minutes. default = 5 years (not implemented)
     #- filtration-status: { type: device, debug: false, widget: text, tags: 'piscine-page', source: myESPs-status, id: "04a87b", name: "Status filtration piscine" }
     - filtration-status: { type: device, debug: false, widget: text, tags: 'piscine-page', source: ESPs, id: "/WiFi/ESPRouter_04a87b/system/status", name: "Status filtration piscine" }
     - pieuvre: { type: device, widget: toggle, tags: 'piscine-page', source: myZibase, id: "ZP8", name: "Pieuvre" }
     - chlore_plus: { type: device, widget: toggle, tags: 'piscine', source: myZibase, id: "ZA14", name: "Chlore +" }
     - chlore_moins: { type: device, widget: toggle, tags: 'piscine', source: myZibase, id: "ZO14", name: "Chlore -" }
     - temperature: { type: sensor, widget: "text!{value, number, :: .# } °C", tags: 'piscine-page, piscine-temp-page', source: ZStick, id: "12-49-3-Air temperature", 
        persistence: "mongo:temperature_piscine:0:aggregate:120000", 
        name: "Température" }
        #id:TTL:raw|aggregate:KEEP    
        # TTL: 0=default, 1h
        # KEEP: in minutes. default = 5 years (not implemented)
     - temperature-graph : { type: variable, widget: temp-graph:piscine.temperature, tags: 'piscine-temp-page', name: "Courbe de température"} 
     # - couleur : { type: variable, widget: color, tags: 'piscine', name: "Couleur de la température"} 
     - couleur : { type: variable, widget: 'text!<span style="background-color:%s;color:black">%s</span>', tags: 'piscine', name: "Couleur de la température"} 
     - filtration-timeline : { type: variable, debug: false, widget: vis-graph, tags: 'piscine-page', name: "Périodes de filtration"} 
     - planning-filtration-graph : { type: variable, debug: false, widget: graph, tags: 'piscine-page', name: "Filtration"} 
     - filtration-couleur : { type: variable, widget: color, tags: 'piscine', name: "Couleur de la filtration"} 
     - filtration-icon : { type: variable, widget: text, tags: 'piscine', name: "Icône de la filtration"} 
     - filtration-message : { type: variable, widget: text, tags: 'piscine', name: "Message de la filtration"} 
     - filtration-start1 : { type: variable, widget: text, tags: 'piscine', name: "Heure 1 de démarrage de la filtration"} 
     - filtration-stop1 : { type: variable, widget: text, tags: 'piscine', name: "Heure 1 d'arrêt de la filtration"} 
     - filtration-start2 : { type: variable, widget: text, tags: 'piscine', name: "Heure 2 de démarrage de la filtration"} 
     - filtration-stop2 : { type: variable, widget: text, tags: 'piscine', name: "Heure 2 d'arrêt de la filtration"} 
     - filtration-duration : { type: variable, widget: "text!{value, number, .##}", tags: 'piscine', name: "Durée de la filtration (heures)"} 
     - durees-filtration : { type: variable, widget: text, tags: 'piscine', name: "Tableau des durées de filtration [°C, h]"} 
     - durees-filtration-graph : { type: variable, debug: false, widget: graph, tags: 'piscine-page', name: "Durées de filtration"} 

  - petit_portail:
    - controle : { type: variable, widget: "confirm:Actionner:primary:Petit portail:Actionner le petit portail?:Partiel,Grand,Non", tags: 'portails-page, entrée', name: "Actionner le petit portail"} 
    - partiel: { type: device, widget: text, tags: 'portails', source: ZStick, id: "16-37-1-currentValue", name: "Petit Portail", camera: camera_exterieure }
    - grand: { type: device, widget: text, tags: 'portails', source: ZStick, id: "16-37-2-currentValue", name: "Petit Portail ouvert en grand", camera: camera_exterieure }
  - grand_portail:
    - controle : { type: variable, widget: "confirm:Actionner:primary:Grand portail:Actionner le grand portail?:Grand,Non", tags: 'portails-page, entrée', name: "Actionner le grand portail"} 
    - grand: { type: device, widget: text, tags: 'portails', source: myZibase, id: "ZA10", name: "Grand Portail" }
    - senseur_fermeture: { type: sensor, widget: text, tags: 'détecteurs, portails', source: myZibase, id: "ZB9", name: "Universal sensor Grand Portail", transform: !!js/function 'function (newValue) {
        return newValue == "1" ? "OFF" : "ON";
      }' }

  - tondeuse:
    - start : { type: variable, widget: toggle, id: unused, tags: tondeuse-page, name: "Marche/Arrêt" }
    - do-start : { type: device, widget: text, source: robonect-command, id: unused, tags: tondeuse-page, name: "Activation Marche/Arrêt" }
    - reset-error : { type: variable, widget: "multistate:RESET:danger", tags: tondeuse-page, name: "Réinitialisation erreur" }
    - do-reset-error : { type: device, widget: text, source: robonect-error, id: error-code, tags: tondeuse-page, 
      transform: !!js/function 'function (value) { 
          if (value == "doRESET") return value;
          return value!=0?"RESET":"NOERROR";
        }',
      name: "Appel reset erreur" }
    - error-code : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/error/code", tags: tondeuse, name: "Error (code)" }
    - error-message : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/error/message", tags: tondeuse-page, name: "Error" }
    - weather : { type: sensor, source: robonect-weather, widget: text, tags: tondeuse-page, id: tmnuweather, name: "Météo" }
    - stopped : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/stopped", tags: tondeuse, name: "Arrêté" }
    - stopped-plain : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/stopped", tags: tondeuse-page, transform: "true=>Arrêté,false=>Marche", name: "Marche/Arrêt" }
    - mode : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/mode", tags: tondeuse, name: "Mode (code)" }
    - mode-plain : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/mode", tags: tondeuse-page, name: "Mode", transform: "0=>Auto,1=>Manuel,2=>Retour à la base" }
    - status-code : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/status", tags: tondeuse, name: "Status (code)" }
    - status-simple : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/status", tags: tondeuse-page, name: "Status simple", transform: "0=>En cours de détermination,1=>En stationnement,2=>En tonte,3=>En recherche de station,4=>En charge,5=>En recherche,7=>En erreur,8=>Signal boucle perdu,16=>Eteint,17=>En veille" }
    - status-plain : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/status/plain", tags: tondeuse-page, name: "Status" }
    - status-since-raw : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/status/duration", tags: tondeuse-page, name: "Status depuis (min)" }
    - status-since : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/status/duration", tags: tondeuse-page, name: "Status depuis", transform: !!js/function 'function (value) { 
            function displayAsDuration(duration) {
              var d = Math.floor(duration/60/24);
              var h = Math.floor((duration - 24*60*d)/60);
              var mn = Math.round(duration - d*24*60 - 60*h);

              var str = "";
              if (d>0) str = d + "j";
              if (h>0) str += (str!=""?" ":"") + h + "h";
              if (mn>0) str += (str!=""?" ":"") + mn + "mn";
              if (d==0 && h===0 && mn===0) str = "0mn";
              return str;
            }
            return displayAsDuration(parseInt(value));
      }' }
    - charge-plain : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/battery/charge", tags: tondeuse, name: "Charge batterie (valeur)", transform: !!js/function 'function (value) { 
      let v = parseInt(value);
      if (isNaN(v)) v = 0;
      return v;
    }' }
    - charge : { type: sensor, source: robonect, widget: progress-bar, id: "/Robonect/mower/battery/charge", tags: tondeuse-page, name: "Charge batterie", transform: !!js/function 'function (value) { 
      let v = parseInt(value);
      if (isNaN(v)) v = 0;
      // should return <value>:<meter|progress>:<min>:<low>:<high>:<optimum>:<max>
      return v + ":meter:0:33:66:90:100";
    }' }
    - charge-progress-color : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/battery/charge", tags: tondeuse, name: "Charge batterie (couleur)", transform: !!js/function 'function (value) { 
      let v = parseInt(value);
      if (isNaN(v)) v = 0;
      // should return <value>:<meter|progress>:<min>:<low>:<high>:<optimum>:<max>
      if (v > 98) return "transparent";
      if (v > 75) return "green";
      if (v > 25) return "orange";
      if (v >= 0) return "red";
    }' }

    - tension-batterie : { type: sensor, source: robonect, widget: text, id: "/Robonect/health/voltage/batt", tags: tondeuse-page, name: "Tension batterie" }
    - next-start : { type: sensor, source: robonect, widget: text, id: "/Robonect/mower/timer/next/unix", tags: tondeuse-page, name: "Prochaine tonte", transform: !!js/function 'function (value) { let v = parseInt(value); return v>0?new Date(v*1000 + (new Date()).getTimezoneOffset() * 60 * 1000 /* for some reason the unix date returned by the robonect is not correct */):"Non défini"; }' }

  - tempo:
    - option_active : { type: variable, widget: toggle, tags: "tempo, aquarium-page", name: "Option tempo active" }
    - couleur_du_jour : { type: device, widget: tempo-color, tags: 'tempo', source: tempo, id: couleurDuJour, name: "Couleur du jour" }
    - couleur_de_demain : { type: device, widget: tempo-color, tags: 'tempo', source: tempo, id: couleurDeDemain, name: "Couleur de demain" }

  - hall-escalier:
    - controle : { type: variable, widget: "multistate:AUTO,MANU:primary,primary", tags: 'lampes', name: "Minuterie hall/escalier"} 
    - sensors: { type: group, widget: walkingman, tags: 'détecteurs', function: !!js/function 'function (newValues, callback) {
            return callback(null, newValues.filter(v => v=="1").length > 0?"ON":"OFF");
          }', name: "Détecteurs hall/escalier", taglist: "sensor-timer"}
    - timeout : { type: variable, widget: text, tags: '', name: "Timeout hall/escalier (mn)"} 
    - extinction : { type: variable, widget: text, tags: '', name: "Heure d'extinction hall/escalier"} 


  - cuisine:
    - controle : { type: variable, widget: "multistate:AUTO,MANU:primary,primary", tags: 'lampes', name: "Minuterie cuisine"} 
    - timeout : { type: variable, widget: text, tags: '', name: "Timeout cuisine (mn)"} 
    - extinction : { type: variable, widget: text, tags: '', name: "Heure d'extinction cuisine"} 


  - say : { type: variable, widget: text, tags: voix, name: "Message parlé"} 
  - say-volatile : { type: device, widget: text, source: voice, id: unused, tags: voix, name: "Message parlé par Google"} 
  - messages-of-the-day : { type: variable, widget: text:%j, tags: voix, name: "Messages de la journée"} 
  - repeat : { type: variable, widget: "multistate:Répéter:primary", tags: 'voix', name: "Répéter le dernier message"} 
  - say-messages-of-the-day : { type: variable, widget: "multistate:Messages du jour:primary", tags: 'voix', name: "Dire les messages du jour"} 
  - text-to-say : { type: variable, widget: "input", tags: 'voix', name: "Message à dire"} 

  - zwave:
     - controller : { type: device, widget: "multistate:INCLUSION,INCLUSION_NON_SECURE,EXCLUSION,NO_INCLUSION_EXCLUSION:secondary,secondary,danger,primary:Inclure,Inclure non séc.,Exclure,Stop", tags: 'zwave', source: ZStick, id: "1", attribute: "inclusion_mode", name: "Controleur"} 
     - config : { type: device, widget: "zwave-config", tags: 'zwave', source: ZStick, id: "1", attribute: "zwave_config", name: "ZWave config"} 
     - config-text : { type: device, widget: "text:%j", tags: 'zwave', source: ZStick, id: "1", attribute: "zwave_config", name: "ZWave config"} 


scenarios:
  - demo:
    - presence_simulator:
        debug: true
        triggers:
          - at: startup
        actions:
          - !!js/function 'function f(callback) {
            //return callback(null);
            var self = this;
            function pgm(device) {
              var onAt = 20000 + 60000 * Math.random();
              var offAt = onAt + 2000 + 30000 * Math.random();
              self.isReleased() || setTimeout(() => {
                self.isReleased() || self.setDeviceState(device, "0");}, onAt);
              self.isReleased() || setTimeout(() => {
                self.isReleased() || self.setDeviceState(device, "1"); pgm(device);}, offAt);
            }
            pgm("hall");
            pgm("escalier");
            pgm("cuisine");
            pgm("bureau");
            callback(null);
           }'
    - pool-temp_simulator:
        debug: true
        triggers:
          - cron: */30 * * * * *
        actions:
          - !!js/function 'function pool_temp(callback) {
              let MIN = -2; // min temperature
              let MAX = 32; // max temperature

              let prev_temp = parseFloat(this.getDeviceState("piscine.temperature") || (Date.now() % ((MAX - MIN)*10))/10 + MIN, 10);

              let temp = ((prev_temp * 10) % 2 == 0)?0.2:-0.2;
              temp = temp + prev_temp;

              if (temp > MAX) temp = MAX - 0.1;
              if (temp < MIN) temp = MIN;

              temp = temp.toFixed(1);

              this.setDeviceState("piscine.temperature", temp, callback);
            }'
  - aquarium:
    - set_end:
      triggers:
        - at: startup
        - cron: 01 00 00 * * *
      actions:
          - { device: aquarium.lampes_end, state: 23:30 }
          - { device: aquarium.pompes_end, state: 23:59 }
    - controle:
      - init:
        triggers:
          - at: startup
        actions:
            - { device: aquarium.controle, state: "AUTO" }
      - ON:
        triggers:
          - state: aquarium.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "ON"}
        actions:
            - { device: aquarium.lampes, state: "ON" }
            - { device: aquarium.pompes, state: "ON" }
      - OFF:
        triggers:
          - state: aquarium.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "OFF"}
        actions:
            - { device: aquarium.lampes, state: "OFF" }
            - { device: aquarium.pompes, state: "OFF" }
    - tempo:
      - set_Bleu:
          triggers:
            - state: tempo.couleur_du_jour
            - state: tempo.option_active
            - state: sunset_plus_30mn
          conditions:
            - {operator: 'or',
                left: {operator: =, left: tempo.couleur_du_jour, right: "Bleu"},
                right: {operator: "!=", left: tempo.option_active, right: "ON"}
            }
          actions:
            - {device: aquarium.pompes_start, state: sunset}
            - {device: aquarium.lampes_start, state: sunset_plus_30mn}
      - set_others:
          triggers:
            - state: tempo.couleur_du_jour
            - state: tempo.option_active
          conditions:
            - {operator: "!=", left: tempo.couleur_du_jour, right: "Bleu"}
            - {operator: =, left: tempo.option_active, right: "ON"}
          actions:
            - {device: aquarium.pompes_start, state: "21:30"}
            - {device: aquarium.lampes_start, state: "22:00"}
    - lampes:
      - start:
          triggers:
            - at: aquarium.lampes_start
          conditions:
            - {operator: =, left: aquarium.controle, right: "AUTO"}
          actions:
            - { device: aquarium.lampes, state: "ON" }
      - stop:
          triggers:
            - cron: 00 30 23 * * * 
          conditions:
            - {operator: =, left: aquarium.controle, right: "AUTO"}
          actions:
            - { device: aquarium.lampes, state: "OFF" }
    - pompes:
      - start:
          triggers:
            - at: aquarium.pompes_start
          conditions:
            - {operator: =, left: aquarium.controle, right: "AUTO"}
          actions:
            - { device: aquarium.pompes, state: "ON" }
      - stop:
          triggers:
            - cron: 00 59 23 * * *
          conditions:
            - {operator: =, left: aquarium.controle, right: "AUTO"}
          actions:
            - { device: aquarium.pompes, state: "OFF" }
  - astronomy:
    - sunset_calculation:
      triggers:
        - at: startup
        - state: sunset
      actions:
        - !!js/function 'function f(callback) {
          let sunset = new Date(this.getDeviceState("sunset"));
          this.setDeviceState("sunset_plus_30mn", new Date(sunset.getTime() + 30 * 60 * 1000), callback);
        }'   
    - day:
      triggers:
        - at: sunrise
      actions:
        - { device: daynight, state: "jour"}
    - night:
      triggers:
        - at: sunset
      actions:
        - { device: daynight, state: "nuit"}
    - day-night:
      triggers:
        - state: sunset
        - state: sunrise
      conditions:
        - { operator: "!=", left: sunset, right: undefined}
        - { operator: "!=", left: sunrise, right: undefined}
      actions:
        - !!js/function 'function f(callback) {
          let sunset = new Date(this.getDeviceState("sunset"));
          let sunrise = new Date(this.getDeviceState("sunrise"));

          let now = Date.now();
          this.setDeviceState("daynight", (sunrise.getTime() < now && now < sunset.getTime())?"jour":"nuit", callback);
        }'   
  - presence:
    - init:
      triggers:
        - at: startup
      actions:
        - { device: alarme.presence-timeout, state: 10 }
    - run:
      - move:
        triggers:
          - state: alarme.sensors
        conditions:
          - { operator: =, left: alarme.sensors, right: "ON" }
        actions:
          - { device: alarme.presence, state: "ON"} 
      - still:
        triggers:
          - state: alarme.sensors
        conditions:
          - { operator: =, left: alarme.sensors, right: "OFF" }
        actions:
          - { device: alarme.fin-presence, state: new Date(Date.now() + this.getDeviceState("alarme.presence-timeout") * 60 * 1000)}
      - long-still:
        triggers:
          - at: alarme.presence-timeout

        actions:
          - { device: alarme.presence, state: "OFF"}





  - semeria:
    - nb_connected_calculation:
        triggers:
          - state: semeria.hosts-connected
        actions:
          - !!js/function 'function f(callback) {
            let list = this.getDeviceState("semeria.hosts-connected");
            //console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>", list);
            let tab = [];
            if (list) {
              tab = list.split(", ");
              //console.log(">>>>>>>>>>>>>>>>>>>>>>>>", tab);
            }
            this.setDeviceState("semeria.nb_connected", tab.length, callback);
          }'   
    - hosts-connected_changed:
        triggers:
          - state: semeria.hosts-connected
        conditions:
          - {operator: "=", left: semeria.voix, right: "ON"}
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
        actions:
          - !!js/function 'function f(callback) {
              let newlist = this.getDeviceState("semeria.hosts-connected");
              let newtab = [];
              if (newlist) newtab = newlist.split(", ");

              let oldlist = this.getDevicePreviousState("semeria.hosts-connected");
              let oldtab = [];
              if (oldlist) oldtab = oldlist.split(", ");

              oldtab.forEach(n => {
                if (newtab.indexOf(n) == -1) {
                  this.setDeviceState("say", n + " n" + String.fromCharCode(39) + "est plus au Séméria");
                }
              });
              newtab.forEach(n => {
                if (oldtab.indexOf(n) == -1) {
                  this.setDeviceState("say", n + " est maintenant au Séméria");
                }
              });

              let n = newtab.length;

              if (n == 0) {
                this.setDeviceState("say", "Il n" + String.fromCharCode(39) + "y a maintenant plus personne au Séméria");
              } else {
                let nstr = "";
                if (n==1) nstr = "une";
                else if (n==2) nstr = "deux";
                else if (n==3) nstr = "trois";
                else if (n==4) nstr = "quatre";
                else if (n==5) nstr = "cinq";
                else nstr = n;
                this.setDeviceState("say", "Il y a maintenant " + nstr + " personne" + (n>1?"s":"") + " au Séméria");
              }

              callback(null);
            }'
    - presence:
        triggers:
          - state: semeria.nb_connected
        conditions:
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
        actions:
            - { device: semeria.presence, state: this.getDeviceState("semeria.nb_connected")>0?"ON":"OFF"}
            - { device: semeria.presence-couleur, state: this.getDeviceState("semeria.nb_connected")>0?"#33cd5f":"lightgrey"}
            - { device: semeria.connected-last-changed-date, state: new Date }
    - presence-changed:
        triggers:
          - state: semeria.presence
        conditions:
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
        actions:
            - { device: semeria.presence-last-changed-date, state: new Date }
    - connected-last-changed-date:
        triggers:
          - state: semeria.nb_connected
          - state: semeria.connected-last-changed-date
          - cron: 00 00 00 * * * # to force recomputation of friendly date
        actions:
            - !!js/function 'function setSemeriaPresenceMessage (callback) {
              function displayAsTime(dateVal) {
                let now = new Date();
                let hh = dateVal.getHours();
                let mm = dateVal.getMinutes();
                let ss = dateVal.getSeconds();
                let yyyy = dateVal.getFullYear();
                let MM = dateVal.getMonth() + 1;
                let dd = dateVal.getDate();

                if (dateVal.getFullYear() === now.getFullYear()) {
                    if (dateVal.getMonth() === now.getMonth()) {
                        if (dateVal.getDate() === now.getDate()) return (hh<10?"0":"")+hh+":"+(mm<10?"0":"")+mm;
                        else if (dateVal.getDate() === now.getDate()-1) str = "hier";
                        else if (dateVal.getDate() === now.getDate()+1) str = "demain";
                        else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                    } else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                } else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                str += " " + (hh<10?"0":"")+hh+":"+(mm<10?"0":"")+mm;
                return str;
              }            

              let lastDateChanged = this.getDeviceState("semeria.connected-last-changed-date");
              let _lastDateChanged = lastDateChanged && new Date(lastDateChanged);

              let nb = this.getDeviceState("semeria.nb_connected");

              let msg = "<h1>" + nb + "</h1> connection" + (nb>1?"s":""); 
              if (_lastDateChanged) {
                msg += " depuis " + displayAsTime(_lastDateChanged);
              }
              this.setDeviceState("semeria.presence-message", msg, callback);
            }'
    - update-presence-timeline:
      triggers:
        - at: startup
        - cron: 00 * * * * * # reload every minutes
        - state: semeria.hosts-connected
      actions:
        - !!js/function 'function updatePresenceTimeline(callback) {
            debug = false;

            getDeviceStateHistory("semeria.hosts-connected", "none", new Date(1900, 0, 1), new Date(3000, 0, 1), (err, res) => {

            // for debugging purposes
              if (false) res = [
                { date: "2021-08-16T07:54:42.252Z", value: "iPhone de Bruno, Android, User 1" },
                { date: "2021-08-16T10:54:42.252Z", value: "iPhone de Bruno, User 1" },
                { date: "2021-08-16T11:10:42.252Z", value: "" },
                { date: "2021-09-01T10:54:42.252Z", value: "Android" },
                { date: "2021-09-01T15:54:42.252Z", value: "Android, User 2" },
                { date: "2021-09-01T20:54:42.252Z", value: "User 2, iPhone de Bruno" },
                { date: "2021-09-01T21:00:42.252Z", value: "User 2" },
                { date: "2021-09-01T21:10:42.252Z", value: "User 2, iPhone de Bruno" },
                { date: "2021-09-01T21:11:42.252Z", value: "User 2" },
                { date: "2021-09-01T21:20:42.252Z", value: "" },
                { date: "2021-09-01T20:25:42.252Z", value: "User 2, iPhone de Bruno" },
                { date: "2021-09-01T22:54:42.252Z", value: "" },
                { date: "2021-09-16T07:54:42.252Z", value: "iPhone de Bruno" },
              ];

              if (!res) return callback(null);

              let tab = res.sort((a, b) => { 
                const A = new Date(a.date).getTime();
                const B = new Date(b.date).getTime();
                return A > B ? 1 : A < B ? -1 : 0;
              })/*.map(r => {
                return { 
                  date: r.date, 
                  value: JSON.parse(r.value).filter(lh => lh.active && lh.reachable && lh.vendor_name != "Freebox SAS").map(lh => lh.primary_name).join(", ")
                }
              })*/;

              debug && console.log("history:", tab);

              let colorIndex = 0;
              let colors = [ "", 
                "background-color:#FFB0B0;border-color:#FF0000;", 
                "background-color:#B0FFB0;border-color:#00FF00;", 
                "background-color:#B0B0FF;border-color:#0000FF;", 
                "background-color:#B0FFFF;border-color:#00FFFF;", 
                "background-color:#FFB0FF;border-color:#FF00FF;", 
                "background-color:#FFFFB0;border-color:#FFFF00;"
               ];

              function getColor() {
                let c = colors[colorIndex];
                colorIndex = (colorIndex + 1) % colors.length;
                return c;
              }

              let id = 0;
              let itemColors = [];

              function record(start, end, content) {
                return {
                      start: start,
                      end: end,
                      content: content,
                      //group: content,
                      style: `overflow: visible;${itemColors[content]}`,
                      id: id++
                }
              }

              let items = [];
              let startTab = [];
              tab.forEach((item, pos, ary) => {

                let newlist = item.value;
                let newtab = [];
                if (newlist) newtab = newlist.split(", ");

                let oldlist = pos > 0 ? ary[pos - 1].value : "";
                let oldtab = [];
                if (oldlist) oldtab = oldlist.split(", ");

                oldtab.forEach(n => {
                  if (newtab.indexOf(n) == -1) {
                    // end of presence detected
                    items.push(new record (ary[startTab[n]].date, ary[pos].date, n));
                    startTab[n] = undefined;
                  }
                });
                newtab.forEach(n => {
                  if (oldtab.indexOf(n) == -1) {
                    // track new presence (if really new)
                    if (startTab[n] === undefined) startTab[n] = pos;
                    if (itemColors[n] === undefined) itemColors[n] = getColor();
                  }
                });
              });

              Object.keys(startTab).forEach(n => {
                if (startTab[n] !== undefined) {
                    items.push(new record (tab[startTab[n]].date, (new Date()).toISOString(), n));
                }
              });

              debug && console.log("items:", items);

              let lastPresenceDate = items.length>0?new Date(items[items.length - 1].end):new Date();
              let now = new Date();

              let data = {
                items: items,
                options: {
                      orientation: "top",
                      width: "400px",
                      editable: false,
                      stack: true,
                      groupEditable: false,
                      selectable: false,
                      zoomMin: 60 * 60 * 1000, // 1 hour
                      start: new Date(lastPresenceDate.getFullYear(), lastPresenceDate.getMonth(), 1, 0, 0, 0, 0).toISOString(),
                      end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0).toISOString(),
                    },
              }

              this.setDeviceState("semeria.presence-timeline", JSON.stringify(data), callback);
            });
          }'
  - home:
    - couleur-telephone:
        triggers:
          - at: startup
          - state: home.appel-en-cours
        actions:
          - { device: home.couleur-telephone, state: this.getDeviceState("home.appel-en-cours")==""?"lightgrey":"#ef473a"}
    - appel-en-cours:
        debug: false
        triggers:
          - state: home.appel-en-cours
          - at: startup
        actions:
          - !!js/function 'function f(callback) {

            debug=true;

            let appel_en_cours = this.getDeviceState("home.appel-en-cours");
            //appel_en_cours = "00390494906746"; // italie
            //appel_en_cours = "0160862732"; // Paris
            //appel_en_cours = "0973055227"; // Dangereux
            //appel_en_cours = "0607186716"; // known
            //appel_en_cours = "Inconnu"; // numéro inconnu
            //appel_en_cours = "0680488523"; // test
            if (appel_en_cours) {

              debug && console.log("appel_en_cours:", appel_en_cours)

              function retrieveFromWebsite(source, cb, url, field_tagRe_transform, localdebug) {
                (debug || localdebug) && console.log("%s doing request ", source, url);
                require("https").get(url, res => {
                  let data = ""  
                  res.setEncoding("utf8");
                  res.on("data", (chunk) => {
                    data += chunk
                  });
                  res.on("end", () => {

                    function textInTag(data, balise) {
                      let startPos = data.search(balise);
                      // console.log(source, startPos);
                      // console.log(source, data.substr(startPos))
                      return startPos>=0?data.substr(startPos).replace(balise, "").replace(/( *<[^>]*> *)*([^<]*).*/, "$2").trim():"";
                    }

                    // for testing purposes
                    let valid_data_118712="<div class=!item-content!> <div  class=!item_number!> <div class=!poi icon-pin! > <p class=!icon icon-geoloc-pin!></p> <p class=!numero!>1</p> <p class=!pinshadow!></p> </div> </div> <div class=!item-info!> <h2 class=!titre! itemprop=!name!> <a data-tealium-link=!1! data-tealium-zone=!resultat! href=!/ppart/Alpes-maritimes-06/Le-golfe-juan-06220/Chabrier_gerard-0493632270_8E0090F00001R10300E10011G! id=!result_1! title=!Chabrier Gérard! target=!_blank!>                            Chabrier Gérard            </a> </h2> <h3 class=!activity!>                                    Particulier                            </h3> <div class=!address! itemprop=!address! itemscope itemtype=!http://schema.org/PostalAddress!> <h3> <span class=!streetAddress! itemprop=!streetAddress! data-nosnippet>495 avenue Pierre Sémard,</span> <span itemprop=!postalCode!>06220</span> <span itemprop=!addressLocality!>LE GOLFE JUAN</span> </h3> </div> <div class=!actions-btn!> <a data-tealium=!telephone_1! href=!javascript:;! class=!item_phone button special-phone! title=!Appeler Chabrier Gérard au 04 93 63 22 70! onclick=!oss118.popinSpecialPhone.open(this);! data-title=!Appeler Chabrier Gérard au 04 93 63 22 70! data-href=!tel:0493632270! data-wording=!<span class=!sr-only!>Appeler Chabrier Gérard au </span>04 93 63 22 70! data-special-phone=!No direct marketing! ><span class=!icon icon-no-market! title=!Opposé au démarchage téléphonique! aria-hidden=!true!></span><span class=!sr-only!>Appeler Chabrier Gérard au </span><span itemprop=!telephone! class=!button_wording nomobile! data-nosnippet>04 93 63 22 70</span> <span class=!button_wording mobile_only!>Appeler<span class=!sr-only!>  Chabrier Gérard au 04 93 63 22 70</span></span> </a> </div> </div> </div> </div> <div class=!right-item!> <div class=!table!> <div class=!image_container!> <div class=!icon_container!> <div class=!icon_propart_container!> <span class=!icon-part! aria-hidden=!true!>".replace(/!/g, String.fromCharCode(34));
                    let error_data="errored data";
                    let valid_data_ctqui="<table width=!550! border=!0! align=!center! cellpadding=!5! cellspacing=!0!><tr><td background=!/image/hautcadre3.gif!><div align=!center!><font color=!#FFFFFF! size=!3! face=!Arial, Helvetica, sans-serif!><strong>Resultat de la recherche pour le : 04 93 64 95 20</strong></font></div></td></tr><tr><td background=!/image/milieucadre3.gif! valign=!middle!><b><center><font color=!#FF0000! size=!1! face=!Arial, Helvetica, sans-serif!>Cette information vous est gracieusement offerte par CTQui gr&acirc;ce &agrave; nos sponsors et à votre aide !<br />Pour maintenir ce service Gratuit depuis bientôt 15 ans, pensez &agrave; d&eacute;couvrir leurs offres ci-dessous<br />et surtout, faîtes savoir que vous appréciez CTQui en cliquant sur !J aime! ou !Partager!</font></center></b><div style=!background: #fff; margin: 20px auto; padding: 15px 20px; width: 86%; border-radius: 20px;-webkit-border-radius: 20px;-moz-border-radius: 20px;!><font color=!#006699! size=!4! face=!Arial, Helvetica, sans-serif!><strong>Chabrier Bruno</strong></font><font color=!#006699! size=!3! face=!Arial, Helvetica, sans-serif!><br />21 B, Chemin De L!estelle<br />06110 Le Cannet<br /><br />Tel : 04 93 64 95 20<br />Fax : 09 58 05 63 02<br />Type : Particulier</font></div><div style=!font-family:Arial, Helvetica, sans-serif; font-size:14px; background: #fff; margin: 0 auto 15px; padding: 10px 8px; width: 90%; border-radius: 10px;-webkit-border-radius: 10px;-moz-border-radius: 10px;!><div style=!float:right;width:120px;!><iframe src=!//www.facebook.com/plugins/like.php?href=http%3A%2F%2Fwww.ctqui.com&amp;width&amp;layout=button&amp;action=like&amp;show_faces=false&amp;share=true&amp;height=35! scrolling=!no! frameborder=!0! style=!border:none; overflow:hidden; height:35px;! allowTransparency=!true!></iframe></div>  Vous appréciez CTQui, faîtes le savoir !</div><div style=!background: #fff; margin: 5px auto; padding: 10px 8px; width: 90%; border-radius: 10px;-webkit-border-radius: 10px;-moz-border-radius: 10px;!><table width=!100%! border=!0!><tr><td width=!9%!><a href=!http://www.awin1.com/awclick.php?gid=309016&mid=7315&awinaffid=290867&linkid=625633&clickref=! target=!_blank!><img src=!/image/internet.gif! width=!36! height=!36! border=!0! alt=!! /></a></td><td width=!91%!><font size=!2! face=!Arial, Helvetica, sans-serif!><strong><a href=!http://www.awin1.com/awclick.php?gid=309016&mid=7315&awinaffid=290867&linkid=625633&clickref=! target=!_blank!><font color=!#006699!>Fibre - Surfez à très grande vitesse pour 19,99€/mois !</font></a><br /><font color=!#FF0000!>TV + Internet + Téléphone fixe</font><font color=!#006699! size=!1! face=!Arial, Helvetica, sans-serif!><br />(SFR Box ou SFR Play Gratuit + SFR Presse + Appels illimités Fixe et Mobile. Très Haut débit 200 Mb/s)</font></strong></font></td> </tr></table><br /><br /></div></td></tr><tr><td style=!background:url(!image/bascadre3.gif!) no-repeat top center! height=!5!></td></tr></table>".replace(/!/g, String.fromCharCode(34));
                    //data = valid_data_118712;
                    //data = error_data;
                    //data = valid_data_ctqui;

                    //(debug || localdebug) && console.log("%s end:", source, data);
                    data = data.replace(/\n|\r/gm," ");
                    
                    let result = { source: source };
                    let allvalues = "";
                    field_tagRe_transform.forEach(ftt => {
                      result[ftt.field] = textInTag(data, ftt.regexp);
                      if (ftt.transform) result[ftt.field] = ftt.transform(result[ftt.field]);
                      allvalues += result[ftt.field];
                    });
                    (debug || localdebug) && console.log(source, ": result=", result);
                    (debug || localdebug) && console.log("allvalues", allvalues);
                    (debug || localdebug) && console.log("cb called with args:", allvalues==""?"no result from " + source:null, result);
                    cb(allvalues==""?"no result from " + source:null, result);
                  });
                }).on("error", err => {
                  console.warn(`${source}: Cannot connect to "${url}"`, err);
                  cb(err, null);
                });
              }

              require("async").parallel([
                function checkDanger(cb) {
                  //return cb(null, "toto");
                  if (appel_en_cours == "Inconnu") return cb(null, {});
                  retrieveFromWebsite("numeroinconnu.fr", cb, "https://www.numeroinconnu.fr/numero/" + appel_en_cours,
                    [
                      {
                        field: "danger",
                        regexp: /<div id="progress-bar-inner" class="progress-bar-rank[0-9]*">/
                      },
                      {
                        field: "pays",
                        regexp: /<td class="[^"]+" itemprop="addressCountry">/
                      },
                      {
                        field: "ville",
                        regexp: /<span itemprop="addressLocality">/
                      },
                    ],
                    false
                  );
                },
                function getNameLocality(cb) {

                  // use the first positive response
                  require("async").tryEach([
                    function handleSpecialNumbers(cb) {
                      let known_nums = {
                        "Inconnu": {},
                        "0607186716": { name: "Bruno" },
                        "0615457163": { name: "Marie-Pierre" },
                        "0477506276": { name: "Grand-mère", locality: "Fontaneilles" },
                        "0952361243": { name: "Grand-mère", locality: "Golfe" },
                        "0493632270": { name: "Grand-mère", locality: "Golfe" },
                        "0492182000": { name: "Alerte mairie" },
                      }
                      debug && console.log("Special number:", known_nums[appel_en_cours])
                      cb(known_nums[appel_en_cours]?null:"not a special number", known_nums[appel_en_cours]);
                    },
                    function retrieveFrom118712(cb) {
                      //return cb("disabled");
                      retrieveFromWebsite("118712", cb, "https://annuaire.118712.fr/?s=" + appel_en_cours,
                        [
                          { field: "name", regexp: /<h2 class=.titre. itemprop=.name.>/ }, 
                          { field: "locality", regexp: /<span itemprop="addressLocality">/ }
                        ]);
                    },
                    function retrieveFromPagesJaunes(cb) {                  
                      //return cb("disabled");
                      retrieveFromWebsite("pagesjaunes", cb, "https://www.pagesjaunes.fr/annuaireinverse/recherche?quoiqui=" + appel_en_cours,
                        [
                          { field: "name", regexp: /<a class="denomination-links pj-lb pj-link"[^>]*>/ }, 
                          { field: "locality", regexp: /<div class="adresse-container noTrad">/, 
                            transform: locality => {
                              return locality.replace(/[^,]*, +[0-9]+ /, "");
                            }
                          }
                        ]);
                    },
                    function retrieveFromCTQui(cb) {                  
                      retrieveFromWebsite("ctqui", cb, "https://www.ctqui.com/resultat.asp?part=www.ctqui.com&num=" + appel_en_cours,
                        [
                          { field: "name", regexp: /<div style="background: #fff; margin: 20px auto; [^>]*>/ }, 
                          { field: "locality", regexp: /<div style="background: #fff; margin: 20px auto; [^<]+<font[^<]+<strong[^<]+<\/strong><\/font><font[^<]+<br[^<]+<br[^>]*>/,
                            transform: locality => {
                              return locality.replace(/[0-9]+ /, "");
                            }
                          }
                      ]);
                    },
                  ], cb);
                }], function (err, results) {
                  debug && console.log("err:", err, ", results:", results)
                  danger = results[0] && results[0].danger || 0;
                  country = results[0] && results[0].pays || "";
                  city = results[0] && results[0].ville || "";
                  name = results[1] && results[1].name || "";
                  locality = results[1] && results[1].locality || "";

                  debug && console.log("danger=", danger);
                  debug && console.log("country=", country);
                  debug && console.log("city=", city);
                  debug && console.log("name=", name);
                  debug && console.log("locality=", locality);

                  let from = locality;
                  if (from == "") {
                    if (country == "" || country.toLowerCase() == "france") {
                      from = city;
                    } else from = country;
                  }

                  let message = appel_en_cours + ":" + danger + ":" + name + ":" + from; 

                  if (from != "") from = " de " + from;
                  debug && console.log("from=", from);

                  let danger_str = "";
                  if (danger >= 75) danger_str = "très douteux";
                  else if (danger > 0) danger_str = "douteux";
                  debug && console.log("danger_str=", danger_str);

                  let say = "";

                  let num = appel_en_cours; 

                  let name_str = name;
                  if (name_str == "") {
                    if (appel_en_cours.match(/^0[1-9][0-9]{8}$/)) {
                      // normal unknown number
                      num = "";
                      for (i = 0; i < 10; i+=2) {
                          num += appel_en_cours[i] + appel_en_cours[i+1] + " ";
                      }
                      num = num.substr(0, num.length - " ".length);
                      name_str = num;
                    } else name_str = num;
                  } 
                  if (name_str != "") name_str = " de " + name_str;
                  debug && console.log("name_str=", name_str);

                  if (name_str == "" && from == "")  {
                    say = "Appel " + danger_str + " en cours";
                  } else {
                    say = "Appel " + danger_str + name_str + from;
                  }
                  this.setDeviceState("home.caracteristiques-dernier-appel", message);
                  debug && console.log("say:", say);
                  this.setDeviceState("say", say );
                  callback(err);
                } 
              );
            }

            // for some reason, console doesn t survive past the end of the script,
            // hence we log on a file
            if (false) { // should not be needed anymore since console.log repaired in load.ts
            var stream = require("fs").createWriteStream("/tmp/freebox_output.txt", {flags:"a"});
            console.log = (...args) => { 
              let stackLine = (new Error).stack.split("\n")[2];
              let [atfunction, vm, lineInFunction, char] = [ "", "", 0, 0 ];
              if (stackLine.indexOf("(") >= 0) {
                [atfunction, vm, lineInFunction, char] = stackLine.split(/[\():]/);
              } else {
                [atfunctionNotUsed, lineInFunction, char] = stackLine.split(/[\():]/);
              }
              let newargs = [];
              newargs.push("%s" + args[0]);
              newargs.push(this.scriptFile + ":" + (this.scriptLine + parseInt(lineInFunction) - 1) + " (" + atfunction.replace(/^ *at ([^ ]+) *$/, "$1") + ") ");
              newargs = newargs.concat(args.slice(1));

              stream.write(require("util").format(...newargs)); 
              stream.write("\n"); 
            }
            }
          }'
    - message-dernier-appel:
        triggers:
          - cron: 00 00 00 * * *  # every midnight, to update the friendly time
          - state: home.dernier-appel
          - state: home.date-dernier-appel
          - state: home.caracteristiques-dernier-appel
        actions:
          - !!js/function 'function message_dernier_appel(callback) {

              function displayAsTime(dateVal) {
                let now = new Date();
                let hh = dateVal.getHours();
                let mm = dateVal.getMinutes();
                let ss = dateVal.getSeconds();
                let yyyy = dateVal.getFullYear();
                let MM = dateVal.getMonth() + 1;
                let dd = dateVal.getDate();

                if (dateVal.getFullYear() === now.getFullYear()) {
                    if (dateVal.getMonth() === now.getMonth()) {
                        if (dateVal.getDate() === now.getDate()) return (hh<10?"0":"")+hh+":"+(mm<10?"0":"")+mm;
                        else if (dateVal.getDate() === now.getDate()-1) str = "hier";
                        else if (dateVal.getDate() === now.getDate()+1) str = "demain";
                        else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                    } else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                } else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                str += " " + (hh<10?"0":"")+hh+":"+(mm<10?"0":"")+mm;
                return str;
              }            
              
              let dernier_appel = this.getDeviceState("home.dernier-appel");
              let date_dernier_appel = this.getDeviceState("home.date-dernier-appel");

              if (!dernier_appel || !date_dernier_appel) return this.setDeviceState("home.message-dernier-appel", "", callback);

              if (dernier_appel.match(/^[0-9]{10}$/)) {
                num = "";
                for (i = 0; i < 10; i+=2) {
                    num += dernier_appel[i] + dernier_appel[i+1] + "&nbsp;";
                }
                num = num.substr(0, num.length - "&nbsp;".length);
              } else num = dernier_appel;

              let caracteristiques_dernier_appel = this.getDeviceState("home.caracteristiques-dernier-appel");
              let updatedate_caracteristiques_dernier_appel = this.getDeviceLastUpdateDate("home.caracteristiques-dernier-appel");
              let danger = 0;
              let name = "";
              let from = "";
              console.log("car date", updatedate_caracteristiques_dernier_appel, "appel", date_dernier_appel);
              if (caracteristiques_dernier_appel && updatedate_caracteristiques_dernier_appel && date_dernier_appel
                && updatedate_caracteristiques_dernier_appel.getTime() > date_dernier_appel.getTime()) {
                let t = caracteristiques_dernier_appel.split(":");
                console.log("num,danger,name,from=", t);
                if (t && t[0] == dernier_appel) {
                  danger = t[1];
                  name = t[2];
                  from = t[3];
                }
              }
              let color = "black"
              if (danger > 0) color = "orange";
              if (danger > 75) color = "red";
              let info = "";
              if (name != "") info = name + (from != ""?" (" + from + ")":"")
              else if (from != "") info = from;
              if (info != "") info += "</BR>";

              console.log("info=",info)
              this.setDeviceState("home.message-dernier-appel", "<span style=color:" + color + ">" + info + num + "</span><BR/>" + displayAsTime(date_dernier_appel), callback);
            }'





  - lampes:
    - lampes-couleur:
      triggers:
        - at: startup
        - state: lampes.nb_lampes_ON
      actions:
          - !!js/function 'function f(callback) {
            let nb = this.getDeviceState("lampes.nb_lampes_ON");
            let color = "lightgrey"
            if (nb == 0) color = "#33cd5f"; // green
            else color = "#ef473a";           // red
            this.setDeviceState("lampes.couleur", color, callback);
          }'
  - piscine:
    - couleur:
        triggers:
          - state: piscine.temperature
        actions:
          - !!js/function 'function f(callback) {
            let temp = parseFloat(this.getDeviceState("piscine.temperature"), 10);
            var colors = [
              [ 0, "#ef473a"],    // red
              [ 0.1, "#886aea"],  // violet
              [ 28, "#33cd5f"],   // green
              [ 30, "#33cd5f"],   // green
              [ 33, "#ef473a"],   // red
            ];

            var color;
            if (temp <= colors[0][0]) {
              color = colors[0][1]
            } else if (temp >= colors[colors.length - 1][0]) {
              color = colors[colors.length - 1][1]
            } else {
              // find closest temp interval
              for (i = 0; i < colors.length - 1; i++) {
                if (temp >= colors[i][0] && temp < colors[i + 1][0]) {
                  // apply a gradient
                  color1 = colors[i][1];
                  r1 = parseInt(color1.substr(1, 2), 16);
                  g1 = parseInt(color1.substr(3, 2), 16);
                  b1 = parseInt(color1.substr(5, 2), 16);
                  color2 = colors[i + 1][1];
                  r2 = parseInt(color2.substr(1, 2), 16);
                  g2 = parseInt(color2.substr(3, 2), 16);
                  b2 = parseInt(color2.substr(5, 2), 16);
                  ratio = (temp - colors[i][0])/(colors[i + 1][0] - colors[i][0])
                  r = Math.round(r1 + ratio * (r2-r1));
                  g = Math.round(g1 + ratio * (g2-g1));
                  b = Math.round(b1 + ratio * (b2-b1));
                  color = "#" + r.toString(16) + g.toString(16) + b.toString(16);
                }
              }
            }
            this.setDeviceState("piscine.couleur", color, callback);
          }'   
    - controle:
      - init:
        triggers:
          - at: startup
        actions:
          - {device: piscine.controle, state: "AUTO" }
          - {device: piscine.filtration-icon, state: "timer" }
          - {device: piscine.filtration-couleur, state: "lightgrey" } 
      - AUTO:
        triggers:
          - state: piscine.controle
          - state: piscine.filtration
          - state: piscine.filtration-duration
        conditions:
          - {operator: =, left: piscine.controle, right: "AUTO"}
        actions:
          - {device: piscine.filtration-icon, state: "timer" }
      - ON:
        triggers:
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "ON"}
        actions:
          - { device: piscine.filtration, state: "ON" }
      - OFF:
        triggers:
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "OFF"}
        actions:
          - { device: piscine.filtration, state: "OFF" }
    - filtration-message:
      # computes filtration-message
      - init:
        triggers:
          - at: startup
        conditions:
          - {operator: =, left: piscine.filtration-message, right: undefined}
        actions:
          - {device: piscine.filtration-message, state: "En attente..." } 
      - AUTO:
        # computes filtration-message
        # when the pool is in AUTO mode
        triggers:
          - state: piscine.controle
          - state: piscine.filtration
          - state: piscine.filtration-start1
          - state: piscine.filtration-duration
        conditions:
          - {operator: =, left: piscine.controle, right: "AUTO"}
          - {operator: "!=", left: piscine.filtration, right: undefined}
          - {operator: "!=", left: piscine.filtration-start1, right: undefined}
          - {operator: "!=", left: piscine.filtration-stop1, right: undefined}
          - {operator: "!=", left: piscine.filtration-duration, right: undefined}
        actions:
          - !!js/function 'function setFiltrationMessage (callback) {
            debug=false;

            let msg = "";
            let filtration = this.getDeviceState("piscine.filtration");
            let duration = parseFloat(this.getDeviceState("piscine.filtration-duration"), 10) * 60;

            function displayAsTime(dateVal) {
              let now = new Date();
              let hh = dateVal.getHours();
              let mm = dateVal.getMinutes();
              let ss = dateVal.getSeconds();
              let yyyy = dateVal.getFullYear();
              let MM = dateVal.getMonth() + 1;
              let dd = dateVal.getDate();

              if (dateVal.getFullYear() === now.getFullYear()) {
                  if (dateVal.getMonth() === now.getMonth()) {
                      if (dateVal.getDate() === now.getDate()) return (hh<10?"0":"")+hh+":"+(mm<10?"0":"")+mm;
                      else if (dateVal.getDate() === now.getDate()-1) str = "hier";
                      else if (dateVal.getDate() === now.getDate()+1) str = "demain";
                      else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
                  } else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
              } else str = (dd<10?"0":"")+dd+"/"+(MM<10?"0":"")+MM+"/"+yyyy;
              str += " " + (hh<10?"0":"")+hh+":"+(mm<10?"0":"")+mm;
              return str;
            }            
            function displayAsDuration(duration) {
              var h = Math.floor(duration/60);
              var mn = Math.round(duration - 60*h);

              var str = "";
              if (h>0 && mn===0) str = h + "h";
              if (h>0 && mn>0) str = h + "h " + mn + "mn";
              if (h===0 && mn>0) str = mn + "mn";
              if (h===0 && mn===0) str = "0mn";
              return str;
            }
            let durationLength = displayAsDuration(duration);

            if (filtration != "ON") {
              let start1 = this.getDeviceState("piscine.filtration-start1");
              let _start1 = start1 && new Date(start1);
              debug && console.log("_start1=", _start1);
              let start2 = this.getDeviceState("piscine.filtration-start2");
              let _start2 = start2 && new Date(start2);
              debug && console.log("_start2=", _start2);
              let start = _start1 && _start1 > Date.now() ? _start1 : (_start2 && _start2 > Date.now() ? _start2 : undefined );
              debug && console.log("start=", start);
              if (start) {
                let startTime = displayAsTime(start);
                let currentDuration = start == _start1? duration - 60: 60;
                let durationLength = displayAsDuration(currentDuration);
                msg = `Mise en<BR/>marche<br><strong>${startTime}</strong><br>pour ${durationLength}`;
              } else {
                msg = `Arrêt<br>permanent`;
              }
            } 
            if (filtration == "ON") {
              let stop1 = this.getDeviceState("piscine.filtration-stop1");
              let _stop1 = stop1 && new Date(stop1);
              let stop2 = this.getDeviceState("piscine.filtration-stop2");
              let _stop2 = stop2 && new Date(stop2);
              let stop = _stop1 && _stop1 > Date.now() ? _stop1 : (_stop2 && _stop2 > Date.now() ? _stop2 : undefined );
              if (stop) {
                let stopTime = displayAsTime(stop);
                let currentDuration = stop == _stop1? duration - 60: 60;
                let durationLength = displayAsDuration(currentDuration);
                msg = `Arrêt<br><strong>${stopTime}</strong><br>Durée<br>${durationLength}`;
              } else {
                msg = `Marche<br>permanente`;
              }
            }
            this.setDeviceState("piscine.filtration-message", msg, callback);
          }'
      - ON:
        triggers:
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "ON"}
        actions:
          - {device: piscine.filtration-message, state: "" } 
      - OFF:
        triggers:
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "OFF"}
        actions:
          - {device: piscine.filtration-message, state: "" }
    - filtration-couleur:
      - AUTO-Offline:
        triggers:
          - at: startup
          - state: piscine.filtration-status
          - state: piscine.filtration
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "AUTO"}
          - {operator: =, left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "orange" } 
          - {device: piscine.filtration-icon, state: "timer" }
      - AUTO-ON:
        triggers:
          - at: startup
          - state: piscine.filtration-status
          - state: piscine.filtration
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "AUTO"}
          - {operator: =, left: this.getDeviceState("piscine.filtration"), right: "ON"}
          - {operator: "!=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "#33cd5f" } # green
          - {device: piscine.filtration-icon, state: "timer" }
      - AUTO-OFF:
        triggers:
          - at: startup
          - state: piscine.filtration-status
          - state: piscine.filtration
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "AUTO"}
          - {operator: =, left: this.getDeviceState("piscine.filtration"), right: "OFF"}
          - {operator: "!=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "lightgrey" } 
          - {device: piscine.filtration-icon, state: "timer" }
      - ON-Offline:
        triggers:
          - at: startup
          - state: piscine.filtration-status
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "ON"}
          - {operator: =, left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "orange" } 
          - {device: piscine.filtration-icon, state: "flash" }
      - ON-ON:
        triggers:
          - at: startup
          - state: piscine.filtration-status
          - state: piscine.filtration
          - state: piscine.controle
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "ON"}
          - {operator: =, left: this.getDeviceState("piscine.filtration"), right: "ON"}
          - {operator: "!=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "#33cd5f" } # green
          - {device: piscine.filtration-icon, state: "flash" }
      - ON-OFF:
        triggers:
          - at: startup
          - state: piscine.controle
          - state: piscine.filtration
          - state: piscine.filtration-status
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "ON"}
          - {operator: =, left: this.getDeviceState("piscine.filtration"), right: "OFF"}
          - {operator: "!=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "#ef473a" } # red
          - {device: piscine.filtration-icon, state: "flash" }
          - {device: piscine.controle, state: "OFF" }
      - OFF-Offline:
        triggers:
          - at: startup
          - state: piscine.controle
          - state: piscine.filtration-status
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "OFF"}
          - {operator: =, left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "orange" } 
          - {device: piscine.filtration-icon, state: "power" }
      - OFF-OFF:
        triggers:
          - at: startup
          - state: piscine.controle
          - state: piscine.filtration
          - state: piscine.filtration-status
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "OFF"}
          - {operator: =, left: this.getDeviceState("piscine.filtration"), right: "OFF"}
          - {operator: "!=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "#ef473a" } # red
          - {device: piscine.filtration-icon, state: "power" }
      - OFF-ON:
        triggers:
          - at: startup
          - state: piscine.controle
          - state: piscine.filtration
          - state: piscine.filtration-status
        conditions:
          - {operator: =, left: this.getDeviceState("piscine.controle"), right: "OFF"}
          - {operator: =, left: this.getDeviceState("piscine.filtration"), right: "ON"}
          - {operator: "!=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - {device: piscine.filtration-couleur, state: "#ef473a" } # red
          - {device: piscine.filtration-icon, state: "power" }
          - {device: piscine.controle, state: "ON" }
    - filtration-duree-start:
      - init:
        triggers:
          - at: startup
        actions:
          # tableau des durées de filtration [ °C, durée en heures ]
          - {device: piscine.durees-filtration, state: "[
              [13, 3],
              [16, 7],
              [20, 11],
              [24, 15],
              [27, 20],
              [30, 24]
            ]
            "}
      - calcul-duree:
        triggers:
          - state: piscine.temperature
        conditions:
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
        actions:
          - !!js/function 'function f(callback) {
            let temp = parseFloat(this.getDeviceState("piscine.temperature"), 10);
            let durees = JSON.parse(this.getDeviceState("piscine.durees-filtration"));

            var duree;
            if (temp <= durees[0][0]) {
              duree = durees[0][1]
            } else if (temp >= durees[durees.length - 1][0]) {
              duree = durees[durees.length - 1][1]
            } else {
              // find closest temp interval
              for (i = 0; i < durees.length - 1; i++) {
                if (temp >= durees[i][0] && temp < durees[i + 1][0]) {
                  // apply a gradient
                  duree1 = durees[i][1];
                  duree2 = durees[i + 1][1];
                  ratio = (temp - durees[i][0])/(durees[i + 1][0] - durees[i][0]);
                  duree = duree1 + ratio * (duree2-duree1);
                }
              }
            }
            // avidunwanted stop/start
            if (duree > 23 + 50/60) duree=24;
            this.setDeviceState("piscine.filtration-duration", duree, callback);
          }'        
      - calcul-start:
        triggers:
          - state: zenith
          - state: piscine.filtration-duration
        conditions:
          - { operator: "!=", left: piscine.filtration-duration, right: undefined}
          - { operator: "!=", left: zenith, right: undefined}
          #- { operator: "!=", left: piscine.filtration, right: "ON"}
        actions:
          - !!js/function 'function f(callback) {
            debug = false;

            let dureeTotale = parseFloat(this.getDeviceState("piscine.filtration-duration"), 10);

            let zenith = new Date(this.getDeviceState("zenith"));

            let previous_start1 = (new Date(this.getDeviceState("piscine.filtration-start1"))).getTime();

            let start2Date = new Date();
            start2Date.setHours(23);
            start2Date.setMinutes(00);
            start2Date.setSeconds(00);
            let start2 = start2Date.getTime();
            let stop2 = start2 + 59 * 60 * 1000;
            // check if next day
            if (stop2 < Date.now()) {
              debug && console.log("2 is next day")
              start2 = start2 + 24 * 60 * 60 * 1000;
              stop2 = stop2 + 24 * 60 * 60 * 1000;
            }

            let duree = dureeTotale - (stop2 - start2) / 60 / 60 / 1000;
            debug && console.log("duree", duree, "total", dureeTotale);

            let start1 = zenith.getTime() - duree * 60 * 60 * 1000 / 2;

            if (start1 < Date.now() + 5 * 60 * 1000) {
              // start1 is almost in the past (5 mn), probably because the temperature is increasing.
              debug && console.log("start1 before 5 mn!!! Let s use previous_start1:", previous_start1)
              start1 = previous_start1;
              // if not defined yet, probably because the first time it is called. Let s start asap
              if (!start1 || new Date(start1).toString() == "InvalidDate") {
                start1 = Date.now() + 3 * 60 * 1000;
                debug && console.log("start1 new Date + 3 = ", start1)
                debug && console.log("previous_start1 was not defined!!! Let s start asap in 3 mn at", start1)
              }
            }

            let stop1 = start1 + duree * 60 * 60 * 1000;
            // check if next day
            if (stop1 < Date.now()) {
              debug && console.log("1 is next day")
              start1 = start1 + 24 * 60 * 60 * 1000;
              stop1 = stop1 + 24 * 60 * 60 * 1000;
            }

            debug && console.log("start1", new Date(start1).toString())
            debug && console.log("stop1", new Date(stop1).toString())
            debug && console.log("start2", new Date(start2).toString())
            debug && console.log("stop2", new Date(stop2).toString())

            // check full time
            if (dureeTotale > 24 - 10/60) {
              stop1 = undefined;
              start2 = undefined;
              stop2 = undefined;
            } else {
              // check overlaps

              // start1 ---------------- stop1
              //              start2 --------------- stop2
              if (start1 && start2 && stop1 && stop2 && start1 < start2 && stop1 > start2 && stop1 < stop2) {
                debug && console.log("chevauchant")
                start2 = undefined;
                stop1 = undefined;
              }

              // start1 ----------------------------- stop1
              //              start2 ----- stop2
              if (start1 && start2 && stop1 && stop2 && start1 < start2 && stop1 > stop2) {
                debug && console.log("inclus")
                start2 = undefined;
                stop2 = undefined;
              }

              // start1 ------ stop1    start2 ----- stop2
              //              <--------------> 
              //                    < 5 mn
              if (stop1 && start2 && Math.abs(stop1 - start2) < 5 * 60 * 1000) {
                debug && console.log("tres prets")
                start2 = undefined;
                stop1 = undefined;
              }

            }

            debug && console.log("=>start1", new Date(start1).toString())
            debug && console.log("=>stop1", new Date(stop1).toString())
            debug && console.log("=>start2", new Date(start2).toString())
            debug && console.log("=>stop2", new Date(stop2).toString())


            debug && console.log(new Date(start1).toString(), new Date(start2).toString())

            this.setDeviceState("piscine.filtration-start1", new Date(start1));
            this.setDeviceState("piscine.filtration-stop1", new Date(stop1));
            this.setDeviceState("piscine.filtration-start2", new Date(start2));
            this.setDeviceState("piscine.filtration-stop2", new Date(stop2), callback);
            //stop2 || callback();
          }'    
      - calcul-start-update:
        triggers:
          - at: piscine.filtration-start1
          - at: piscine.filtration-start2
          - at: startup
        actions:
          - { wait: 10 s }
          - { scenario: piscine.filtration-duree-start.calcul-start, action: start}
    - filtration-start:
      debug: false
      triggers:
        - at: piscine.filtration-start1
        - at: piscine.filtration-start2
      conditions:
        - {operator: =, left: piscine.controle, right: "AUTO"} 
      actions:
        - !!js/function 'function f(callback) {
            let delay = 15 * 1000; // 15s

            let date = this.getDeviceLastUpdateDate("piscine.filtration");

            if (!date || Date.now() - date.getTime() >= delay ) { 
              this.setDeviceState("piscine.filtration", "ON", callback);
            } else {
              setTimeout(() => { f(callback); }, delay);
            }
          }' 
    - filtration-stop:
      debug: false
      triggers:
        - at: piscine.filtration-stop1
        - at: piscine.filtration-stop2
      conditions:
        - {operator: =, left: piscine.controle, right: "AUTO"} 
      actions:
        - !!js/function 'function f(callback) {
            let delay = 15 * 1000; // 15s

            let date = this.getDeviceLastUpdateDate("piscine.filtration");

            if (!date || Date.now() - date.getTime() >= delay ) { 
              this.setDeviceState("piscine.filtration", "OFF", callback);
            } else {
              setTimeout(() => { f(callback); }, delay);
            }
          }' 
    - filtration-offline:
        debug: false
        triggers:
          - state: piscine.filtration-status
        conditions:
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
          - {operator: "=", left: this.msg.newValue, right: "offline"}
        actions:
          - { device: say, state: "Err" }
          - { wait: 5s } # sometimes goes offline then immediately back online
          - { scenario: piscine.warn-if-filtration-really-offline, action: start } 
        else:
          - { scenario: piscine.warn-if-filtration-really-offline, action: stop } 
    - filtration-back-online:
        debug: false
        triggers:
          - state: piscine.filtration-status
        conditions:
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
          - {operator: "=", left: this.msg.newValue, right: "online"}
        actions:
          - { device: piscine.filtration, state: getDeviceState("piscine.filtration")}
    - warn-if-filtration-really-offline:
        debug: false
        triggers:
          - at: startup
        conditions:
          - {operator: "=", left: getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - { device: say, state: "Attention, la filtration de la piscine vient de passer en erreur !" }
    - filtration-offline-at-start-or-stop:
        debug: false
        triggers:
          - at: piscine.filtration-start1
          - at: piscine.filtration-start2
          - at: piscine.filtration-stop1
          - at: piscine.filtration-stop2
        conditions:
          - {operator: "=", left: this.getDeviceState("piscine.filtration-status"), right: "offline"}
        actions:
          - { device: say, state: "Attention, la filtration de la piscine est en erreur !" }
    - update-durees-filtration-graph:
      triggers:
        - state: piscine.durees-filtration
        - state: piscine.temperature
        - state: piscine.filtration-duration
      conditions:
          - {operator: "!=", left: this.msg.newValue, right: ""}
          - {operator: "!=", left: this.msg.newValue, right: null}
          - {operator: "!=", left: this.msg.newValue, right: undefined}
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
      actions:
        - !!js/function 'function f(callback) {

            function HHhMMmm(duree) {
              let h = Math.floor(duree);
              let m = Math.round((duree - h) * 60);
              let s = h + "h";
              if (m > 0) s = s + m +"mn";

              return s;
            }
            let tem = this.getDeviceState("piscine.temperature");
            let temperature = undefined;
            if (tem) {
              temperature = parseFloat(this.getDeviceState("piscine.temperature"));
              temperature = tem;
            }
            let duree = this.getDeviceState("piscine.filtration-duration");
            let durees = JSON.parse(this.getDeviceState("piscine.durees-filtration"));
            let data = {
              chartType: "LineChart",
              dataTable: [["Température", {"role":"annotation"}, {"role":"annotationText"}, "Durée", {role: "style"} ]].concat(
              [
                [ {v: 0, f:"0°C"}, null, null, {v: durees[0][1], f: HHhMMmm(durees[0][1])}, null ],
                ...durees.map(d => [ {v: d[0], f: d[0] + "°C"}, null, null, {v: d[1], f: HHhMMmm(d[1])}, null]),
                [ {v: 40, f:"40°C"}, null, null, {v: durees[durees.length-1][1], f: HHhMMmm(durees[durees.length-1][1])}, null ],
                [ {v: temperature, f: parseFloat(temperature) + "°C"}, "Maintenant", null, {v: duree, f: HHhMMmm(duree)}, "point {fill-color: orange;}" ],
              ].sort((a, b) => { if (isNaN(a[0].v) || isNaN(b[0].v)) return 0; if (a[0].v < b[0].v) return -1; if (a[0].v > b[0].v) return 1; return 0;})),
              options: {
                legend: { position: "none" },
                pointSize: 5,
                vAxis: { title: "Durée", format:"#h", ticks: [ 0, 3, 6, 9, 12, 15, 18, 21, 24] },
                hAxis: { title: "Température", format:"#.#°C", slantedText: false, ticks: [0, 5, 10, 15, 20, 25, 30, 35, 40].concat(temperature?[temperature]:[]) },
                annotations: { style: "line", stem: { color: "orange"} },
              }
            }
            this.setDeviceState("piscine.durees-filtration-graph", JSON.stringify(data), callback);
          }'
    - update-planning-filtration-graph:
      debug: false
      triggers:
        - at: startup
        - cron: 00 * * * * * # reload every minutes
        - state: piscine.filtration-duration
        - state: piscine.filtration-start1
        - state: piscine.filtration-start2
        - state: piscine.filtration-stop1
        - state: piscine.filtration-stop2
      actions:
        - !!js/function 'function f(callback) {
            debug = false;

            function h(d) {
              if (d) {
                let _d = new Date(d);
                //debug && console.log("=", _d.toString());
                let hh = _d.getHours() + _d.getMinutes()/60 + _d.getSeconds()/3600;
                //debug && console.log("=>", hh);
                return hh
              }
              return undefined;
            }

            function formatHour(hour) {
              let h = Math.floor(hour);
              let m = Math.round((hour - h) * 60);
              let str = (h<10?"0":"") + h + ":" + (m<10?"0":"") + m;

              return str;
            }

            function formatDuration(duration) {
              if (!duration) return "<non définie>";

              let h = Math.floor(duration);
              let m = Math.round((duration - h) * 60);
              let str = (h>0?h+"h":"") + (m>0?m+"mn":"");

              return str;
            }

            let duration = this.getDeviceState("piscine.filtration-duration");

            let _start1 = this.getDeviceState("piscine.filtration-start1");
            let _stop1 = this.getDeviceState("piscine.filtration-stop1");
            let _start2 = this.getDeviceState("piscine.filtration-start2");
            let _stop2 = this.getDeviceState("piscine.filtration-stop2");
            
            let start1 = _start1 && _start1.toString() != "InvaidDate" && new Date(_start1).getTime() || undefined;
            let stop1 = _stop1 && _stop1.toString() != "InvaidDate" && new Date(_stop1).getTime() || undefined;
            let start2 = _start2 && _start2.toString() != "InvaidDate" && new Date(_start2).getTime() || undefined;
            let stop2 = _stop2 && _stop2.toString() != "InvaidDate" && new Date(_stop2).getTime() || undefined;
            
            let prev_stop1 = stop1 && stop1 - 24 * 60 * 60 * 1000;
            let prev_stop2 = stop2 && stop2 - 24 * 60 * 60 * 1000;

            let state24;
            let _d24 = new Date();
            _d24.setHours(23);
            _d24.setMinutes(59);
            _d24.setSeconds(0);
            let d24 = _d24.getTime();
            let now = new Date().getTime();

            // handle the case where start1 is already tomorrow.
            // we translate it back to today
            if (start1 > d24) {
              start1 = start1 - 24 * 60 * 60 * 1000;
              stop1 = stop1 && stop1 - 24 * 60 * 60 * 1000;
            } 

            debug && console.log("============================");
            debug && console.log("start1:", _start1);
            debug && console.log("stop1:", _stop1);
            debug && console.log("prev_stop1:", prev_stop1 && new Date(prev_stop1).toString());
            debug && console.log("start2:", _start2);
            debug && console.log("stop2:", _stop2);
            debug && console.log("prev_stop2:", prev_stop2 && new Date(prev_stop2).toString());
            debug && console.log("d24:", new Date(d24).toString());
            debug && console.log("now:", new Date(now).toString());


            let _data;

            let ON = { v: 1, f: "ON"};
            let OFF = { v: 0, f: "OFF"};

            if (!start1 && !stop1 && !start2 && !stop2) {
              debug && console.log("no date");
              _data = [
                [ {v: 0, f: formatHour(0)}, null, null, OFF, false ],
                [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, false ],
                [ {v: 24, f: formatHour(24)}, null, null, OFF, false ],
              ];
            } else if (!stop1 && !start2 && !stop2) {
              // full time
              debug && console.log("full time");
              _data = [
                [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
              ];
            } else {
              if (!start2 && !stop1) {
                // start1 ---------------- stop1
                //              start2 --------------- stop2
                debug && console.log("overlap, start2 before stop1");
                if (d24 <= stop2) {
                  // ON at midnight
                  if (now < prev_stop2) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: h(prev_stop2), f: formatHour(h(prev_stop2))}, null, null, ON, true ],
                      [ {v: h(prev_stop2), f: formatHour(h(prev_stop2))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
                    ];
                  } else if (now < start1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                      [ {v: h(prev_stop2), f: formatHour(h(prev_stop2))}, null, null, ON, true ],
                      [ {v: h(prev_stop2), f: formatHour(h(prev_stop2))}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
                    ];
                  } else {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                      [ {v: h(prev_stop2), f: formatHour(h(prev_stop2))}, null, null, ON, true ],
                      [ {v: h(prev_stop2), f: formatHour(h(prev_stop2))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
                    ];
                  }
                } else {
                  // OFF at midnight
                  if (now < start1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                  } else if (now < stop2) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                  } else {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                  }
                }
              } else if (!start2 && !stop2) {
                // start1 ----------------------------- stop1
                //              start2 ----- stop2
                debug && console.log("complete overlap, start2-stop2 inid start1-stop1");
                if (d24 <= stop1) {
                  // ON at idight
                  if (now < prev_stop1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: h(prev_stop1), f: formatHour(h(prev_stop1))}, null, null, ON, true ],
                      [ {v: h(prev_stop1), f: formatHour(h(prev_stop1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
                    ];
                  } else if (now < start1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                      [ {v: h(prev_stop1), f: formatHour(h(prev_stop1))}, null, null, ON, true ],
                      [ {v: h(prev_stop1), f: formatHour(h(prev_stop1))}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
                    ];
                  } else {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, ON, true ],
                      [ {v: h(prev_stop1), f: formatHour(h(prev_stop1))}, null, null, ON, true ],
                      [ {v: h(prev_stop1), f: formatHour(h(prev_stop1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, ON, true ],
                    ];
                  }
                } else {
                  // OFF at idight
                  if (now < start1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                  } else if (now < stop1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                  } else {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                  }
                }
              } else {
                // no overlap
                debug && console.log("no overlap");
                if (now < start1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                } else if (now < stop1) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                } else if (now < start2) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                } else if (now < stop2) {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, ON, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                } else {
                    _data = [
                      [ {v: 0, f: formatHour(0)}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, OFF, true ],
                      [ {v: h(start1), f: formatHour(h(start1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, ON, true ],
                      [ {v: h(stop1), f: formatHour(h(stop1))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, OFF, true ],
                      [ {v: h(start2), f: formatHour(h(start2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, ON, true ],
                      [ {v: h(stop2), f: formatHour(h(stop2))}, null, null, OFF, true ],
                      [ {v: h(now), f: formatHour(h(now))}, "Maintenant", undefined, OFF, true ],
                      [ {v: 24, f: formatHour(24)}, null, null, OFF, true ],
                    ];
                }
              }
            }

            let data = {
              chartType: "AreaChart",
              dataTable: [["Heure", {"role":"annotation"}, {"role":"annotationText"}, "Etat", {role: "certainty"} ]].concat(_data),
              options: {
                legend: { position: "none" },
                vAxis: { ticks: [ 1 ], textPosition: "none", minValue: 0, maxValue: 1 },
                hAxis: { title: "Durée de filtration : " + formatDuration(duration), format:"#h", slantedText: false, ticks: [ 0, 3, 6, 9, 12, 15, 18, 21, 24] },
                annotations: { style: "line", stem: { color: "orange"} },
                height: "200",
              }
            }

            this.setDeviceState("piscine.planning-filtration-graph", JSON.stringify(data), callback);
          }'
    - update-filtration-timeline:
      debug: false
      triggers:
        - at: startup
        - cron: 00 * * * * * # reload every minutes
        - state: piscine.filtration
        - state: piscine.filtration-start1
        - state: piscine.filtration-stop1
        - state: piscine.filtration-start2
        - state: piscine.filtration-stop2
      actions:
        - !!js/function 'function updateFiltrationTimeline(callback) {
            debug = false;

            getDeviceStateHistory("piscine.filtration", "none", new Date(1900, 0, 1), new Date(3000, 0, 1), (err, res) => {

      
              //const tab = res.filter(r => (new Date(r.date).getMonth()===8)&&(new Date(r.date).getDate()===15)).sort((a, b) => { 
              const tab = res && res.sort((a, b) => { 
                const A = new Date(a.date).getTime();
                const B = new Date(b.date).getTime();
                return A > B ? 1 : A < B ? -1 : 0;
              }) || [];

              debug && console.log("history:", tab);

              let items = [];

              // add planification as dashed
              let start1 = getDeviceState("filtration-start1");
              if (start1 === "Invalid Date") start1 = null;
              let stop1 = getDeviceState("filtration-stop1");
              if (stop1 === "Invalid Date") stop1 = null;
              let start2 = getDeviceState("filtration-start2");
              if (start2 === "Invalid Date") start2 = null;
              let stop2 = getDeviceState("filtration-stop2");
              if (stop2 === "Invalid Date") stop2 = null;

              let id = -1;
              const style = "background-color:transparent;border-style:dashed!important;text-align:end;z-index: 0;"
              if (start1 && stop1) {
                items.push({
                  start: start1,
                  end: stop1,
                  content: "Planifié",
                  style: style,
                  id: id--
                });
              }
              if (start2 && stop2) {
                items.push({
                  start: start2,
                  end: stop2,
                  content: "Planifié",
                  style: style,
                  id: id--
                });
              }
              if (start1 && !stop1 && !start2 && stop2) {
                items.push({
                  start: start1,
                  end: stop2,
                  content: "Planifié",
                  style: style,
                  id: id--
                });
              }

              let prev_ON = -1;
              let prev_OFF = -1;
              tab.forEach((item, pos, ary) => {
                if (item.value === "ON" && prev_ON === -1) {
                  prev_ON = pos;
                  prev_OFF = -1;
                }
                if (item.value === "OFF" && prev_OFF === -1) {
                  prev_OFF = pos;
                  if (prev_ON != -1) {
                    items.push({
                      start: ary[prev_ON].date,
                      end: ary[prev_OFF].date,
                      content: "ON",
                      style: "overflow:hidden;",
                      id: pos
                    });
                  }
                  prev_ON = -1;
                }
              });

              let now = new Date();
              //now = new Date(now.getTime() + 1 * 60 * 1000); // now + 1 mn
              if (prev_ON !== -1 && prev_OFF === -1) {
                // still not OFF until now
                items.push({
                  start: tab[prev_ON].date,
                  end: now.toISOString(),
                  content: "ON",
                  style: "overflow:hidden;",
                  id: tab.length
                });
              }

              debug && console.log("items=", items);

              const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 24, 0, 0, 0);

              let data = {
                items: items,
                options: {
                      orientation: "bottom",
                      width: "400px",
                      editable: false,
                      stack: false,
                      groupEditable: false,
                      selectable: false,
                      start: new Date(endOfDay.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
                      end: endOfDay.toISOString()
                    }
              }

              setDeviceState("piscine.filtration-timeline", JSON.stringify(data), callback);

            });

          }'
  - petit_portail:
    - controle:
      - init:
        triggers:
          - at: startup
        actions:
            - { device: petit_portail.controle, state: "OFF" }
      - partiel:
        triggers:
          - state: petit_portail.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "Partiel"}
        actions:
            - { device: petit_portail.partiel, state: "ON" }
            - { device: petit_portail.controle, state: "OFF" }
      - grand:
        triggers:
          - state: petit_portail.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "Grand"}
        actions:
            - { device: petit_portail.grand, state: "ON" }
            - { device: petit_portail.controle, state: "OFF" }
  - grand_portail:
    - controle:
      - init:
        triggers:
          - at: startup
        actions:
            - { device: grand_portail.controle, state: "OFF" }
      - grand:
        triggers:
          - state: grand_portail.controle
        conditions:
          - {operator: =, left: this.msg.newValue, right: "Grand"}
        actions:
            - { device: grand_portail.grand, state: "ON" }
            - { device: grand_portail.controle, state: "OFF" }  
    - wheather-500:
        triggers:
          - state: tondeuse.weather
        conditions:
          - { operator: "=", left: msg.newValue, right: "Erreur 500" }
        actions:
          - { device: say, state: "Erreur 500" }
    - wheather-500-fixed:
        triggers:
          - state: tondeuse.weather
        conditions:
          - { operator: "=", left: msg.oldValue, right: "Erreur 500" }
        actions:
          - { device: say, state: "Erreur 500 réparée" }
    - set-start-stop:
      - start-status: # updates the position of the start toggle
        - start-status-OFF: # updates the position of the start toggle to OFF
            triggers:
              - at: startup
              - state: tondeuse.stopped
            conditions:
              - { operator: "=", left: tondeuse.stopped, right: "true" }
              - { operator: "=", left: tondeuse.start, right: "ON" }
            actions:
              - { device: tondeuse.start, state: "OFF" }
        - start-status-ON: # updates the position of the start toggle to ON
            triggers:
              - at: startup
              - state: tondeuse.stopped
            conditions:
              - { operator: "=", left: tondeuse.stopped, right: "false" }
              - { operator: "!=", left: tondeuse.start, right: "ON" }
            actions:
              - { device: tondeuse.start, state: "ON" }
      - set-start: # start the mower when the start toggle is switched on
          triggers:
            - state: tondeuse.start
          conditions:
            - { operator: "=", left: msg.newValue, right: "ON" }
            - { operator: "=", left: msg.oldValue, right: "OFF" }
            - { operator: "=", left: tondeuse.stopped, right: "true" }
          actions:
            - { device: tondeuse.do-start, state: "ON" }
      - set-stop: # stop the mower when the start toggle is switched off
          triggers:
            - state: tondeuse.start
          conditions:
            - { operator: "=", left: msg.newValue, right: "OFF" }
            - { operator: "=", left: msg.oldValue, right: "ON" }
            - { operator: "=", left: tondeuse.stopped, right: "false" }
          actions:
            - { device: tondeuse.do-start, state: "OFF" }

    - activate-reset-button:
        triggers:
          - at: startup
          - state: tondeuse.error-code
        conditions:
          - { operator: "!=", left: msg.newValue, right: 0 }
        actions:
          - { device: tondeuse.reset-error, state: "RESET" }
        else:
          - { device: tondeuse.reset-error, state: "OFF" }
    - reset-error:
        triggers:
          - state: tondeuse.reset-error
        conditions:
          - { operator: "!=", left: msg.oldValue, right: "RESET" }
          - { operator: "=", left: msg.newValue, right: "RESET" }
        actions:
          - { device: tondeuse.do-reset-error, state: "doRESET" }
          - { device: tondeuse.reset-error, state: "OFF" }
    - tondeuse-injoignable:
        triggers:
          - cron: 0 */15 * * * * # check and notify every 15 mn
        conditions:
          - {operator: "<", left: this.getDeviceLastUpdateDate("tondeuse.status-since-raw").getTime(), right: Date.now() - 15 * 60 * 1000 } # pas mis à jour depuis 15mn
        actions:
          - { device: say, state: "Attention, la tondeuse n'est pas joignable! Elle est peut-être coincée." }
    - tondeuse-erreur:
        triggers:
          - state: tondeuse.status-code
        conditions:
          - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
          - {operator: "or", 
            left: {operator: "=", left: this.msg.newValue, right: 7}, # erreur
            right: {operator: "=", left: this.msg.newValue, right: 8} # signal boucle perdu
          } 
        actions:
          - { device: say, state: "Attention, la tondeuse est en erreur !" }
          - { wait: 15mn }
          - { scenario: tondeuse-erreur-rappel, action: start}
        else:
          - { scenario: tondeuse-erreur-rappel, action: stop}
    - tondeuse-erreur-rappel:
        triggers:
          - at: startup
        conditions:
          - {operator: "or", 
            left: {operator: "=", left: getDeviceState("tondeuse.status-code"), right: 7}, # erreur
            right: {operator: "=", left: getDeviceState("tondeuse.status-code"), right: 8} # signal boucle perdu
          } 
        actions:
          - { device: say-volatile, state: "Attention, la tondeuse est toujours en erreur !" }
          - { wait: 15mn }
          - { scenario: tondeuse-erreur-rappel, action: start}
    - tondeuse-probleme-charge:
        triggers:
          - state: tondeuse.charge
        conditions:
          - {operator: "=", left: this.getDeviceState("tondeuse.status-code"), right: 4} # en charge
          - {operator: "<", left: parseInt(this.msg.newValue), right: 99} # avoids false alerts close to 100%
          - {operator: "<", left: parseInt(this.msg.newValue), right: parseInt(this.msg.oldValue)}
        actions:
          - { device: say, state: "Attention, la tondeuse ne charge pas bien. Essayez de la ré-enclencher dans sa station !" }
    - charge-manu:
        triggers:
          - state: tondeuse.charge
          - state: tondeuse.mode
          - state: tondeuse.status-code
        conditions:
          - {operator: "=", left: tondeuse.mode, right: 1} # manuel
          - {operator: "=", left: tondeuse.status-code, right: 1} # stationnement
          - {operator: "<=", left: parseInt(this.getDeviceState("tondeuse.charge")), right: 15 }
        actions:
          - { device: say, state: "Attention, la tondeuse nécessite une charge manuelle !" }
  - minuteries:
    - controle:
      - init:
        triggers:
          - at: startup
        actions:
            - { device: hall-escalier.controle, state: "AUTO" }
            - { device: cuisine.controle, state: "AUTO" }
    - setup:
      triggers:
        - at: startup
      actions:
        # durée de la minuterie du hall
        - {device: hall-escalier.timeout, state: "3"}
        # durée de la minuterie de la cuisine
        - {device: cuisine.timeout, state: "5"}

    - timer-cuisine:
      - ON:
        triggers:
          - state: cuisine
        conditions:
          - controle: { operator: =, left: cuisine.controle, right: "AUTO" }
          - { operator: =, left: cuisine, right: "1" }
          - night: { operator: =, left: daynight, right: "nuit" }   
        actions:
          - {device: lampes.lampe_table_cuisine, state: "ON"}
          - {device: cuisine.extinction, state: "" }      
      - OFF:
        triggers:
          - at: startup
          - state: cuisine
          - state: cuisine.controle
        conditions:
          - controle: { operator: =, left: cuisine.controle, right: "AUTO" }
          - { operator: "!=", left: cuisine, right: "1" }
        actions:
          - {device: cuisine.extinction, state: new Date(Date.now() + getDeviceState("cuisine.timeout") * 60 * 1000) }      
      - timeout:
        triggers:
          - at: cuisine.extinction
        conditions:
          - controle: { operator: =, left: cuisine.controle, right: "AUTO" }
          - { operator: "!=", left: cuisine.extinction, right: "" }
        actions:
          - {device: lampes.lampe_table_cuisine, state: "OFF"}
    - timer-hall-escalier:
      - hall-ON:
        triggers:
          - state: hall
        conditions:
          - controle: { operator: =, left: hall-escalier.controle, right: "AUTO" }
          - { operator: =, left: hall, right: "1" }
          - night: { operator: =, left: daynight, right: "nuit" }   
        actions:
          - {device: lampes.lampe_hall, state: "ON"}
          - {device: lampes.lampe_escalier, state: "ON"}
          - {device: hall-escalier.extinction, state: "" }
      - escalier-ON:
        triggers:
          - state: escalier
        conditions:
          - controle: { operator: =, left: hall-escalier.controle, right: "AUTO" }
          - { operator: =, left: escalier, right: "1" }
          - night: { operator: =, left: daynight, right: "nuit" }   
        actions:
          - {device: lampes.lampe_escalier, state: "ON"}
          - {device: lampes.lampe_hall, state: "ON"}
          - {device: hall-escalier.extinction, state: "" }      
      - OFF:
        triggers:
          - at: startup
          - state: hall
          - state: escalier
          - state: hall-escalier.controle
        conditions:
          - controle: { operator: =, left: hall-escalier.controle, right: "AUTO" }
          - { operator: "!=", left: hall, right: "1" }
          - { operator: "!=", left: escalier, right: "1" }
        actions:
          - {device: hall-escalier.extinction, state: new Date(Date.now() + getDeviceState("hall-escalier.timeout") * 60 * 1000) }      
      - timeout:
        triggers:
          - at: hall-escalier.extinction
        conditions:
          - controle: { operator: =, left: hall-escalier.controle, right: "AUTO" }
          - { operator: "!=", left: hall-escalier.extinction, right: "" }
        actions:
          - {device: lampes.lampe_escalier, state: "OFF"}
          - {device: lampes.lampe_hall, state: "OFF"}
  - say:
    triggers:
      - state: say
    actions:
      - { device: say-volatile, state: this.msg.newValue }    
      - !!js/function 'function add_to_messages_of_the_day(callback) {
          // update messages-of-the-day
          let motd = this.getDeviceState("messages-of-the-day");  
          // motd is in the form [ {date: Date, message: string }]

          if (!motd || motd=="") {
            motd = [];
          } else {
            motd = JSON.parse(motd);
          }

          // push new message
          motd.push({
            date: (new Date()).toString(),
            message: this.getDeviceState("say")
          });

          this.setDeviceState("messages-of-the-day", JSON.stringify(motd), callback);
      }'
  - clean-messages-of-the-day:
    triggers:
      - at: startup
      - state: messages-of-the-day
      - cron: 00 00 00 * * * # midnight
    actions:
      - !!js/function 'function clean_messages_of_the_day(callback) {
          // remove oldest messages

          let motd = this.getDeviceState("messages-of-the-day");  
          // motd is in the form [ {date: Date, message: string }]

          if (!motd || motd=="") {
            motd = [];
          } else {
            motd = JSON.parse(motd);
          }

          let now = Date.now();
          let today = new Date(now);
          today.setMilliseconds(0);
          today.setSeconds(0);
          today.setMinutes(0);
          today.setHours(0);
          let newmotd = motd.filter(e => (new Date(e.date)).getTime() >= today.getTime());

          if (JSON.stringify(newmotd) != JSON.stringify(motd)) {
            this.setDeviceState("messages-of-the-day", JSON.stringify(newmotd), callback);
          } else {
            callback(null);
          }
      }'
  - repeat:
    triggers:
      - state: repeat
    actions:
      - { device: say-volatile, state: new String("Dernier message: ") + this.getDeviceState("say") } 
  - messages-of-the-day:
    triggers:
      - state: say-messages-of-the-day   
    actions:
      - !!js/function 'function say_messages_of_the_day(callback) {
          let motd = this.getDeviceState("messages-of-the-day");  

          if (!motd || motd == "" || motd == "[]") {
            return this.setDeviceState("say-volatile", "Aucun message aujourd" + String.fromCharCode(39) + "hui", callback);
          }
          motd = JSON.parse(motd); 

          let message = "J" + String.fromCharCode(39) + "ai émis " + motd.length + " messages aujourd" + String.fromCharCode(39) + "hui. ";

          motd.forEach(e => {
            let d = new Date(e.date);
            message += "A " + d.getHours() + " heures " + d.getMinutes() + ", " + e.message + ". ";
          });
          this.setDeviceState("say-volatile", message, callback);
      }'
  - say-text-to-say:
    triggers:
      - state: text-to-say   
    actions:
      - !!js/function 'function say_text_to_say(callback) {
          let text = this.getDeviceState("text-to-say");  

          if (!text || text == "") {
            return callback(null);
          }

          this.setDeviceState("say-volatile", text, callback);
      }'
  - inclusion:
      debug: false
      triggers:
        - state: zwave.controller
      conditions:
        - {operator: "=", left: this.msg.newValue, right: "INCLUSION"}
        - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue }
      actions:
        - { device: say-volatile, state: "Mode inclusion activé" }
  - inclusion-non-secure:
      triggers:
        - state: zwave.controller
      conditions:
        - {operator: "=", left: this.msg.newValue, right: "INCLUSION_NON_SECURE"}
        - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue }
      actions:
        - { device: say-volatile, state: "Mode inclusion non sécurisé activé" }
  - exclusion:
      triggers:
        - state: zwave.controller
      conditions:
        - {operator: "=", left: this.msg.newValue, right: "EXCLUSION"}
        - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
      actions:
        - { device: say-volatile, state: "Mode exclusion activé" }
  - no-inclusion:
      triggers:
        - state: zwave.controller
      conditions:
        - {operator: "=", left: this.msg.newValue, right: "NO_INCLUSION_EXCLUSION"}
        - {operator: "=", left: this.msg.oldValue, right: "INCLUSION"}
        - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
      actions:
        - { device: say-volatile, state: "Mode inclusion désactivé" }
  - no-inclusion-non-secure:
      triggers:
        - state: zwave.controller
      conditions:
        - {operator: "=", left: this.msg.newValue, right: "NO_INCLUSION_EXCLUSION"}
        - {operator: "=", left: this.msg.oldValue, right: "INCLUSION_NON_SECURE"}
        - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
      actions:
        - { device: say-volatile, state: "Mode inclusion non sécurisé désactivé" }
  - no-exclusion:
      triggers:
        - state: zwave.controller
      conditions:
        - {operator: "=", left: this.msg.newValue, right: "NO_INCLUSION_EXCLUSION"}
        - {operator: "=", left: this.msg.oldValue, right: "EXCLUSION"}
        - {operator: "!=", left: this.msg.newValue, right: this.msg.oldValue}
      actions:
        - { device: say-volatile, state: "Mode exclusion désactivé" }

pages:
  - Home:
      title: 'Tableau de bord'
      page: home.html
      args: 
        - widgets: 
          - pool-temp:
            widget: dashboard-icon
            icon: "thermometer" # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            linkPage: PiscineTemp
            label: "Piscine"
            color: "${piscine.couleur}"
            text: "<h1><BR/><strong>${piscine.temperature, number, ::.#}</strong><BR/>°C</h1>"
          - pool-filtration:
            widget: dashboard-icon
            icon: "${piscine.filtration-icon}" # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            linkPage: Piscine
            label: "Filtration ${piscine.filtration}"
            color: "${piscine.filtration-couleur}"
            text: "${piscine.filtration-message}"
          - entrée:
            widget: dashboard-camera
            #url: /assets/imgs/demo_dashboard_entrée.jpg
            camera: camera_exterieure
            linkPage: Entrée
            label: "Entrée"
          - aquarium:
            widget: dashboard-camera
            #url: /assets/imgs/demo_dashboard_aquarium.jpg
            camera: camera_interieure
            linkPage: Aquarium
            label: "Aquarium ${aquarium.lampes}"
          - piscine:
            widget: dashboard-camera
            #url: /assets/imgs/demo_dashboard_piscine.jpg
            camera: camera_piscine
            linkPage: Piscine
            label: Piscine
          - lights:
            widget: dashboard-icon
            icon: "bulb" # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            color: "${lampes.couleur}"
            linkPage: Lampes
            label: "Lampes"
            text: "<h1>${lampes.nb_lampes_ON}/${lampes.nb_lampes}</br></h1>"
          - portails:
            widget: dashboard-icon
            icon: ios-barcode # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            linkPage: Portails
            label: Portails
          - tempo:
            widget: dashboard-tempo
            couleurDuJour: "${tempo.couleur_du_jour}"
            couleurDeDemain: "${tempo.couleur_de_demain}"
            optionActive: "${tempo.option_active}"
            label: "Tempo"           
          - tondeuse:
            widget: dashboard-icon
            icon: "https://hqvcdn3.azureedge.net/qs_ver=20200901T111258&hcsh=521DD67B7F6A78F2B9520C4778134371/_$$_/media/aprimo/gardena/robotic%20lawnmowers/photos/studio/ga310-05xxxx/ga310-0592.png"
            linkPage: Tondeuse
            label: "${tondeuse.status-plain}"
            active: "${tondeuse.start}"
            text: "<div style=position:relative;bottom:-42px;width:100%>${tondeuse.mode-plain}</div><div style=width:${tondeuse.charge-plain}%;height:100%;position:absolute;bottom:-59px;opacity:0.3;background:${tondeuse.charge-progress-color};></div>"
          - presence-semeria:
            widget: dashboard-icon
            icon: home # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            linkPage: Séméria
            label: "Séméria"
            color: "${semeria.presence-couleur}"
            text: "${semeria.presence-message}"
          - voix:
            widget: dashboard-icon
            icon: volume-up # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            label: "Voix"
            linkPage: Voix
          - telephone:
            widget: dashboard-icon
            icon: call # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            linkPage: Téléphone
            label: "Téléphone"
            text: "${home.message-dernier-appel}"
            color: "${home.couleur-telephone}"
          - users:
            widget: dashboard-icon
            icon: people # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            label: "Utilisateurs"
          - debug:
            widget: dashboard-icon
            icon: ios-bug # a valid ionic icon name https://ionicframework.com/docs/v3/ionicons/
            linkPage: Devices
            label: "Debug"
  - Aquarium:
      title: 'Aquarium'
      page: list.html
      args: 
        - tag-list: 'aquarium-page*'
        - headers: false
  - Entrée:
      title: "Entrée"
      page: list.html
      args: 
        - tag-list: 'entrée'
        - headers: false
  - Portails:
      title: "Portails"
      page: list.html
      args: 
        - tag-list: 'portails-page'
        - headers: false
  - Piscine:
      title: "Piscine"
      page: list.html
      args: 
        - tag-list: 'piscine-page'
        - headers: false
        - widgets-override: durees-filtration:graph
  - PiscineTemp:
      title: "Température Piscine"
      page: list.html
      args: 
        - tag-list: 'piscine-temp-page'
        - headers: false
  - Lampes:
      title: 'Lampes'
      page: list.html
      args: 
        - tag-list: 'lampes*:lampes'
        - headers: false
  - Séméria:
      title: 'Séméria'
      page: list.html
      args: 
        - tag-list: 'semeria*:Séméria'
        - headers: false
  - Tondeuse:
      title: 'Tondeuse'
      page: list.html
      args: 
        - tag-list: 'tondeuse-page'
        - headers: false
  - Téléphone:
      title: 'Téléphone'
      page: list.html
      args: 
        - tag-list: 'téléphone-page'
        - headers: false
  - Voix:
      title: 'Voix'
      page: list.html
      args: 
        - tag-list: 'voix'
        - headers: false
  - ZWave:
      title: 'ZWave'
      page: list.html
      args: 
        - tag-list: 'zwave'
        - headers: false
  - Devices:
      title: 'Tous les devices'
      page: list.html
      args: 
        - tag-list: 'alarme, lampes*:lampes, détecteurs, websockets, piscine*:piscine, aquarium*:aquarium, portails*:portails, astronomy, tempo, semeria:séméria, tondeuse*:tondeuse, voix*:voix, téléphone*:téléphone, *:autres'
        - image-size: tiny
  - About:
      menu-item: 'A propos'
      title: 'A propos'
      page: about.html


users:
  - id: 0 # must be unique
    name: 'demouser'
    initials: 'du'
    login: demo
    password: demo
    phone: phone_1
    macaddress: 'e4:b5:26:3a:73:ac'
    avatar: ...
  - id: 1 # must be unique
    name: 'Luc'
    initials: 'LC'
    login: Luc
    password: demo
    phone: phone_1
    macaddress: 'e0:b5:2d:3a:76:ab'
    avatar: ...
  - id: 2 # must be unique
    name: 'Karen'
    initials: 'KC'
    login: Karen
    password: demo
    phone: phone_2
    macaddress: 'd8:c2:6a:bc:b3:2c'
    avatar: ...
  - id: 3 # must be unique
    name: 'Stéphane'
    initials: 'SC'
    login: Stéphane
    password: demo
    phone: phone_3
    macaddress: '40:6A:AE:6F:D7:45'
    avatar: ...
  - id: 4 # must be unique
    name: 'Nathalie'
    initials: 'NC'
    login: Nathalie
    password: demo
    phone: phone_4
    macaddress: 'B1:A2:E7:83:96:CB'
    avatar: ...
