diff --git a/.env b/.env index 5b96cb3..1418e46 100644 --- a/.env +++ b/.env @@ -1,5 +1,5 @@ UID=1000 -GID=1000 +GID=1004 DIR=/srv/docker DOMAIN=milvert.com DOMAIN_PEEK=peekskog.se @@ -30,10 +30,24 @@ LIBRESPEED_PW=NDVlMzA0MzQ0YmM3NTA0OGIxMzEzMjk4 PIHOLE_PW=NmE4Njc4Y2RjMmUxZmIxMWZkOTg1NTZj UUID=1000 -PGID=1000 +PGID=1004 TZ=Europe/Stockholm TGRAMAPI=5306622927:AAGsDD83H1joum_hlTiCtSFWwIyM3nEEntU TGRAMCHAT=-1005306622927 TGRAM_SHOUTRRR_ADDRESS=telegram://5306622927:AAGsDD83H1joum_hlTiCtSFWwIyM3nEEntU@telegram?chats=-1001662562579 + + +VWSFRIEND_USERNAME='admin' +VWSFRIEND_HOSTNAME='vwsfriend' +VWSFRIEND_PASSWORD='bajsa123' +WECONNECT_USER='simon@milvert.com' +WECONNECT_PASSWORD='khLe86qM925qkB' +WECONNECT_SPIN=9331 +WECONNECT_INTERVAL=180 +VW_DB_HOSTNAME='postgres' +VW_DB_NAME='vwfriend' +VW_DB_USER='vwfriend' +VW_DB_PASSWORD='icCJ8iwKJBeRBg' +ADDITIONAL_PARAMETERS=-vv --mqttbroker mqtt --mqttport 1883 -mu simon -mp bajsa123 diff --git a/docker-compose.home.yml b/docker-compose.home.yml index d75fe84..a04b406 100644 --- a/docker-compose.home.yml +++ b/docker-compose.home.yml @@ -54,7 +54,7 @@ services: - PUID=${UID} - PGID=${GID} labels: - - "traefik.enable=true" + - "traefik.enable=false" - "traefik.http.services.landet_domo-service.loadbalancer.server.port=8080" - "traefik.http.routers.landet_domo-secure.entrypoints=web-secure" - "traefik.http.routers.landet_domo-secure.rule=Host(`landet.${DOMAIN}`)" diff --git a/docker-compose.service.yml b/docker-compose.service.yml index aa135da..61889ad 100644 --- a/docker-compose.service.yml +++ b/docker-compose.service.yml @@ -11,7 +11,7 @@ services: max-file: "3" networks: - backend - image: koenkk/zigbee2mqtt:1.30.4 + image: koenkk/zigbee2mqtt:1.32.0 restart: always volumes: - ./zigbee_home_2:/app/data @@ -41,7 +41,7 @@ services: max-file: "5" networks: - backend - image: koenkk/zigbee2mqtt:1.30.4 + image: koenkk/zigbee2mqtt:1.32.0 restart: always volumes: - ${DIR}/zigbee2matt:/app/data @@ -63,30 +63,6 @@ services: - "traefik.http.routers.zig1-secure.middlewares=chain-authelia@file" - "traefik.http.routers.zig1-secure.tls=true" - grafana: - image: grafana/grafana:9.3.6 - container_name: grafana - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "5" - restart: always - networks: - - backend - volumes: - - ./grafana/grafana.ini:/etc/grafana/grafana.ini - # Data persistency - # sudo mkdir -p /srv/docker/grafana/data; chown 472:472 /srv/docker/grafana/data - - "${DIR}/grafana:/var/lib/grafana" - labels: - - diun.enable=true - - "traefik.enable=true" - - "traefik.http.routers.grafana-secure.entrypoints=web-secure" - - "traefik.http.routers.grafana-secure.rule=Host(`data.${DOMAIN}`)" - - "traefik.http.routers.grafana-secure.middlewares=chain-no-auth@file" - - "traefik.http.routers.grafana-secure.tls.certresolver=milvert_dns" - - "traefik.http.routers.grafana-secure.tls=true" influx: image: influxdb:2.6 @@ -195,7 +171,7 @@ services: mqtt: # image: eclipse-mosquitto:1.6.13 - image: eclipse-mosquitto:2.0 + image: eclipse-mosquitto:2.0.17 container_name: mqtt logging: driver: "json-file" @@ -203,7 +179,6 @@ services: max-size: "10m" max-file: "5" networks: - - frontend - backend restart: always ports: @@ -327,10 +302,6 @@ services: restart: unless-stopped networks: backend: - backend_macvlan: - ipv4_address: 10.0.0.201 - pihole_net: - ipv4_address: 10.0.5.3 labels: - diun.enable=true - "traefik.enable=true" @@ -348,15 +319,3 @@ networks: external: true backend: external: false - backend_macvlan: - driver: macvlan - driver_opts: - parent: eno1 - ipam: - config: - - subnet: 10.0.0.200/27 - pihole_net: - driver: bridge - ipam: - config: - - subnet: 10.0.5.0/29 diff --git a/docker-compose.yml b/docker-compose.yml index c3f041d..f1ff927 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,10 @@ secrets: file: $SECRETSDIR/authelia_notifier_smtp_password authelia_duo_api_secret_key: file: $SECRETSDIR/authelia_duo_api_secret_key + vwfriend_password: + file: $SECRETSDIR/vwfriend_password + vwconnect_password: + file: $SECRETSDIR/vwconnect_password ########################### EXTENSION FIELDS ########################## @@ -21,8 +25,8 @@ secrets: ####################################################################### x-environment: &default-tz-puid-pgid - TZ=$TZ - - PUID=$PUID - - PGID=$PGID + - PUID=${UID} + - PGID=${GID} x-common-keys-monitoring: &common-keys-monitoring networks: @@ -85,7 +89,6 @@ services: - ./notify.txt:/tmp/authelia/notification.txt networks: - backend - - frontend ports: # The HTTP port - "9091:9091" @@ -252,9 +255,272 @@ services: #entrypoint: redis-server --appendonly yes --requirepass $REDIS_PASSWORD --maxmemory 512mb --maxmemory-policy allkeys-lru labels: - diun.enable=true + +######################### HOME ############################ +# +# HOME +# +############################################################### + + ha: + container_name: ha + image: homeassistant/home-assistant:2023.8 + restart: always + privileged: true + networks: + backend: + docker_vlan: + ipv4_address: 10.0.0.203 + depends_on: + - postgres + ports: + - 8123:8123 + volumes: + - ./ha/config:/config + - /etc/localtime:/etc/localtime:ro + environment: + - PUID=${UUID} + - PGID=${PGID} + - TZ=${TZ} + labels: + - diun.enable=true + - "traefik.enable=true" + - "traefik.http.routers.ha.entrypoints=web-secure" + - "traefik.http.routers.ha.rule=Host(`ha.${DOMAIN}`)" + - "traefik.http.routers.ha.middlewares=chain-no-auth@file" + - "traefik.http.routers.ha.tls.certresolver=milvert_dns" + - "traefik.http.routers.ha.tls=true" + - "traefik.http.services.ha.loadbalancer.server.port=8123" + + hassconf: + container_name: hassconf + image: causticlab/hass-configurator-docker:0.5.2 + restart: unless-stopped + networks: + - backend + ports: + - 3218:3218 + volumes: + - ./ha/hass_config:/config + - ./ha/config:/hass-config + depends_on: + - ha + labels: + - diun.enable=true + environment: + - PUID=${UUID} + - PGID=${PGID} + - TZ=${TZ} + + appdaemon: + container_name: appdaemon + image: acockburn/appdaemon:4.4.2 + restart: unless-stopped + depends_on: + - ha + labels: + - diun.enable=true + volumes: + - ./ha/appdaemon_config:/conf + - /etc/localtime:/etc/localtime:ro + ports: + - 5050:5050 + networks: + - backend + environment: + - PUID=${UUID} + - PGID=${PGID} + - TZ=${TZ} + # The following values ar overridden in docker-compose.override.yml + - HA_URL=http://ha:8123 + - DASH_URL=http://appdaemon:5050 + - TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmZmM0YTI1ZjVlYWM0NGY5OTA3OGFmOWJiMTJmYmUzZCIsImlhdCI6MTY5MzczMDQwNSwiZXhwIjoyMDA5MDkwNDA1fQ.YVH8WhH6FMvTkecJ-taCACP6kVG9is2hHmTR3tk3cns + + evcc: + command: + - evcc + container_name: evcc + image: evcc/evcc:0.120.0 + ports: + - 7070:7070/tcp + dns: + - 8.8.8.8 + volumes: + - "./evcc/evcc.yaml:/etc/evcc.yaml:ro" + - ./evcc/evcc:/root/.evcc + restart: unless-stopped + networks: + - backend + labels: + - diun.enable=true + - "traefik.enable=true" + - "traefik.http.routers.evcc.entrypoints=web-secure" + - "traefik.http.routers.evcc.rule=Host(`box.${DOMAIN}`)" + - "traefik.http.services.evcc.loadbalancer.server.port=7070" + - "traefik.http.routers.evcc.middlewares=chain-no-auth@file" + - "traefik.http.routers.evcc.tls.certresolver=milvert_dns" + - "traefik.http.routers.evcc.tls=true" + + grafana: + image: grafana/grafana:10.0.0 + container_name: grafana + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" + restart: always + networks: + - backend + volumes: + - ./grafana/grafana.ini:/etc/grafana/grafana.ini + # Data persistency + # sudo mkdir -p /srv/docker/grafana/data; chown 472:472 /srv/docker/grafana/data + - "${DIR}/grafana:/var/lib/grafana" + labels: + - diun.enable=true + - "traefik.enable=true" + - "traefik.http.routers.grafana-secure.entrypoints=web-secure" + - "traefik.http.routers.grafana-secure.rule=Host(`data.${DOMAIN}`)" + - "traefik.http.routers.grafana-secure.middlewares=chain-no-auth@file" + - "traefik.http.routers.grafana-secure.tls.certresolver=milvert_dns" + - "traefik.http.routers.grafana-secure.tls=true" + +######################### WEB ############################ +# +# WEB +# +############################################################### + + librespeed: + image: ghcr.io/linuxserver/librespeed:5.2.5 + container_name: librespeed + environment: + - PUID=${UUID} + - PGID=${PGID} + - TZ=${TZ} + - PASSWORD=${LIBRESPEED_PW} + - CUSTOM_RESULTS=false + - DB_TYPE=sqlite + volumes: + - ./librespeed/config:/config + restart: unless-stopped + networks: + - backend + labels: + - diun.enable=true + - "traefik.enable=true" + - "traefik.http.routers.librespeed.entrypoints=web-secure" + - "traefik.http.routers.librespeed.rule=Host(`librespeed.${DOMAIN}`)" + - "traefik.http.routers.librespeed.middlewares=chain-authelia@file" + - "traefik.http.routers.librespeed.tls.certresolver=milvert_dns" + - "traefik.http.routers.librespeed.tls=true" + + + vwsfriend: + image: tillsteinbach/vwsfriend:0.23.11 + container_name: vwfriend + ports: + - ${VWSFRIEND_PORT-4000}:${VWSFRIEND_PORT-4000} + networks: + - backend + volumes: + - ./vwsfriend_data:/config + environment: + - VWSFRIEND_USERNAME=${VWSFRIEND_USERNAME-admin} + - VWSFRIEND_PASSWORD=${VWSFRIEND_PASSWORD} + - VWSFRIEND_PORT=${VWSFRIEND_PORT-4000} + - WECONNECT_USER + - WECONNECT_PASSWORD + - WECONNECT_SPIN + - WECONNECT_INTERVAL + - DATABASE_URL=postgresql://${VW_DB_USER}:${VW_DB_PASSWORD}@${VW_DB_HOSTNAME}:5432/${VW_DB_NAME} + - ADDITIONAL_PARAMETERS=--with-database --with-abrp ${ADDITIONAL_PARAMETERS} + depends_on: + - postgres + restart: unless-stopped + dns: + - 8.8.8.8 + healthcheck: + test: (wget -qO- http://localhost:${VWSFRIEND_PORT-4000}/healthcheck | grep "ok" -q) || exit 1 + interval: 60s + timeout: 10s + retries: 3 + secrets: + - vwfriend_password + - vwconnect_password + labels: + - diun.enable=true + - "traefik.enable=true" + - "traefik.http.services.vwsfriend-service.loadbalancer.server.port=4000" + - "traefik.http.routers.vwsfriend.entrypoints=web-secure" + - "traefik.http.routers.vwsfriend.rule=Host(`vw.${DOMAIN}`)" + - "traefik.http.routers.vwsfriend.middlewares=chain-no-auth@file" + - "traefik.http.routers.vwsfriend.tls.certresolver=milvert_dns" + - "traefik.http.routers.vwsfriend.tls=true" + + vwgrafana: + image: tillsteinbach/vwsfriend-grafana:0.23.11 + container_name: vwgrafana + ports: + - ${GF_SERVER_HTTP_PORT-3001}:${GF_SERVER_HTTP_PORT-3000} + networks: + backend: + volumes: + - ./vwfriend_grafana/data:/var/lib/grafana + - ./vwfriend_grafana/dashboard:/home/grafana/dummy + environment: + - PUID=${UUID} + - PGID=${PGID} + - TZ=${TZ} + - GF_SECURITY_ADMIN_USER=${VWSFRIEND_USERNAME-admin} + - GF_SECURITY_ADMIN_PASSWORD=${VWSFRIEND_PASSWORD-secret} + - GF_SERVER_HTTP_PORT=${GF_SERVER_HTTP_PORT-3000} + - DB_USER=${VW_DB_USER-admin} + - DB_PASSWORD=${VW_DB_PASSWORD-secret} + - DB_HOSTNAME=${vw_DB_HOSTNAME-postgres} + - DB_PORT=${DB_PORT-5432} + - DB_NAME=${VW_DB_NAME-vwsfriend} + - VWSFRIEND_USERNAME=${VWSFRIEND_USERNAME-admin} + - VWSFRIEND_PASSWORD=${VWSFRIEND_PASSWORD-secret} + - VWSFRIEND_HOSTNAME=${VWSFRIEND_HOSTNAME-vwsfriendbackend} + - VWSFRIEND_PORT=${VWSFRIEND_PORT-4000} + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "(wget -qO- http://localhost:${GF_SERVER_HTTP_PORT-3000}/api/health | grep '\"database\": \"ok\"' -q) || exit 1"] + interval: 60s + timeout: 10s + retries: 3 + labels: + - diun.enable=true + - "traefik.enable=true" + - "traefik.http.services.vwgrafana.loadbalancer.server.port=3000" + - "traefik.http.routers.vwgrafana.entrypoints=web-secure" + - "traefik.http.routers.vwgrafana.rule=Host(`vwgrafana.${DOMAIN}`)" + - "traefik.http.routers.vwgrafana.middlewares=chain-no-auth@file" + - "traefik.http.routers.vwgrafana.tls.certresolver=milvert_dns" + - "traefik.http.routers.vwgrafana.tls=true" + + networks: frontend: external: true backend: external: false +# MACVLAN guide: https://blog.oddbit.com/post/2018-03-12-using-docker-macvlan-networks/ +# ip link add vlan-shim link eno1 type macvlan mode bridge +# ip addr add 10.0.0.223/32 dev vlan-shim +# ip link set vlan-shim up +# ip route add 10.0.0.192/27 dev vlan-shim + docker_vlan: + external: true + driver: macvlan + driver_opts: + parent: eno1 + ipam: + config: + - subnet: 10.0.0.200/27 + +volumes: + vwfriend_grafana: diff --git a/evcc/evcc.yaml b/evcc/evcc.yaml new file mode 100644 index 0000000..baac077 --- /dev/null +++ b/evcc/evcc.yaml @@ -0,0 +1,133 @@ +sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3ODEzODIyNTYsImlhdCI6MTY4Njc3NDI1NiwiaXNzIjoiZXZjYy5pbyIsInN1YiI6Im1pbHZlcnQifQ.HUEmc0NSPt9x5MbOHUAGU6bp3H3E3qwu6O6BAHH9FvE + +#log: debug +#levels: +# easee: trace + +interval: 30s + +vehicles: + - name: car + type: custom + title: id4 + capacity: 79 # kWh + soc: + source: mqtt + topic: weconnect/0/vehicles/WVGZZZE2ZPE051949/domains/fuelStatus/rangeStatus/primaryEngine/currentSOC_pct + range: + source: mqtt + topic: weconnect/0/vehicles/WVGZZZE2ZPE051949/domains/fuelStatus/rangeStatus/primaryEngine/remainingRange_km + onIdentify: # set defaults when vehicle is identified + mode: pv # enable PV-charging when vehicle is identified + minSoC: 20 # immediately charge to 0% regardless of mode unless "off" (disabled) + targetSoC: 90 # limit charge to 90% + +chargers: + - name: wallbox + type: easee + user: simon+easee@milvert.com + password: X7#aEzjlEysBgl + charger: EHCNF485 + +loadpoints: + - title: Garage + charger: wallbox + vehicle: car + mode: pv + phases: 3 + mincurrent: 6 + maxcurrent: 16 + resetOnDisconnect: True + enable: + threshold: 0 + delay: 15s + disable: + threshold: 500 + delay: + 2m + +site: + title: Hemma + meters: + pv: + - pv + grid: my_grid + +meters: + - name: pv + type: custom + power: + source: mqtt + topic: inverter/measure/active_power + jq: .value + energy: + source: mqtt + topic: inverter/calculated/accumulated_yield_energy + jq: .value + timeout: 1h + currents: + - source: mqtt + topic: inverter/measure/phase_A_current + jq: .value + - source: mqtt + topic: inverter/measure/phase_B_current + jq: .value + - source: mqtt + topic: inverter/measure/phase_C_current + jq: .value + + - name: my_grid + type: custom + power: + source: calc + add: + - source: mqtt + topic: dsmr/reading/electricity_currently_returned + scale: -1000 + - source: mqtt + topic: dsmr/reading/electricity_currently_delivered + scale: 1000 + energy: + source: calc + add: + - source: mqtt + topic: dsmr/reading/electricity_returned + scale: 0.001 + - source: mqtt + topic: dsmr/reading/electricity_delivered + scale: -0.001 + + currents: + - source: calc + add: + - source: mqtt + topic: dsmr/reading/phase_currently_delivered_l1 + scale: -1 + - source: mqtt + topic: dsmr/reading/phase_currently_returned_l1 + - source: calc + add: + - source: mqtt + topic: dsmr/reading/phase_currently_delivered_l2 + scale: -1 + - source: mqtt + topic: dsmr/reading/phase_currently_returned_l2 + - source: calc + add: + - source: mqtt + topic: dsmr/reading/phase_currently_delivered_l3 + scale: -1 + - source: mqtt + topic: dsmr/reading/phase_currently_returned_l3 + +influx: + url: http://influx:8086 + database: Car + token: H7m068KDh84kcmmkz3fy-dEsKoeYv90rnNXIidPZoBy240Jzdbr2uDtGveiuDz4rGK4jmMI8J00zVwgIBYJxXA== + org: milvert + +mqtt: + broker: mqtt:1883 + user: simon + password: bajsa123 + topic: evcc diff --git a/evcc/evcc/evcc.db b/evcc/evcc/evcc.db new file mode 100644 index 0000000..e7574c6 Binary files /dev/null and b/evcc/evcc/evcc.db differ diff --git a/ha/appdaemon_config/appdaemon.yaml b/ha/appdaemon_config/appdaemon.yaml new file mode 100644 index 0000000..4eb587f --- /dev/null +++ b/ha/appdaemon_config/appdaemon.yaml @@ -0,0 +1,18 @@ +appdaemon: + latitude: 0 + longitude: 0 + elevation: 30 + time_zone: Europe/Berlin + plugins: + HASS: + type: hass + ha_url: http://ha:8123 + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmZmM0YTI1ZjVlYWM0NGY5OTA3OGFmOWJiMTJmYmUzZCIsImlhdCI6MTY5MzczMDQwNSwiZXhwIjoyMDA5MDkwNDA1fQ.YVH8WhH6FMvTkecJ-taCACP6kVG9is2hHmTR3tk3cns + cert_verify: False + loglevel: DEBUG + module_debug: +http: + url: http://appdaemon:5050 +admin: +api: +hadashboard: diff --git a/ha/appdaemon_config/apps/__pycache__/hello.cpython-310.pyc b/ha/appdaemon_config/apps/__pycache__/hello.cpython-310.pyc new file mode 100644 index 0000000..16f59f3 Binary files /dev/null and b/ha/appdaemon_config/apps/__pycache__/hello.cpython-310.pyc differ diff --git a/ha/appdaemon_config/apps/apps.yaml b/ha/appdaemon_config/apps/apps.yaml new file mode 100644 index 0000000..4c84034 --- /dev/null +++ b/ha/appdaemon_config/apps/apps.yaml @@ -0,0 +1,3 @@ +hello_world: + module: hello + class: HelloWorld diff --git a/ha/appdaemon_config/apps/apps.yaml.example b/ha/appdaemon_config/apps/apps.yaml.example new file mode 100644 index 0000000..4c84034 --- /dev/null +++ b/ha/appdaemon_config/apps/apps.yaml.example @@ -0,0 +1,3 @@ +hello_world: + module: hello + class: HelloWorld diff --git a/ha/appdaemon_config/apps/hello.py b/ha/appdaemon_config/apps/hello.py new file mode 100755 index 0000000..7e44729 --- /dev/null +++ b/ha/appdaemon_config/apps/hello.py @@ -0,0 +1,13 @@ +import hassapi as hass + +# +# Hello World App +# +# Args: +# + + +class HelloWorld(hass.Hass): + def initialize(self): + self.log("Hello from AppDaemon") + self.log("You are now ready to run Apps!") diff --git a/ha/appdaemon_config/compiled/css/default/hello_application.css b/ha/appdaemon_config/compiled/css/default/hello_application.css new file mode 100644 index 0000000..838b403 --- /dev/null +++ b/ha/appdaemon_config/compiled/css/default/hello_application.css @@ -0,0 +1,240 @@ + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + padding: 0; +} + +body { + margin: 0; + background-color: #222; + font-size: 15px; + color: #fff; + padding: 0; + line-height: 1; + font-family: 'Helvetica Neue', 'Helvetica', 'Open Sans', 'Arial' +} + +b, strong { + font-weight: bold; +} + +a { + text-decoration: none; + color: inherit; +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} + +img, object { + max-width: 100%; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px;} + +iframe { + max-width: 100%; +} + +table { + border-collapse: collapse; + border-spacing: 0; + width: 100%; +} + +td { + vertical-align: middle; +} + +ul, ol { + padding: 0; + margin: 0; +} + +h1, h2, h3, h4, h5, p { + padding: 0; + margin: 0; +} +h1 { + margin-bottom: 6px; + text-align: center; + font-size: 100%; + font-weight: 200; +} +h2 { + font-size: 300%; + font-weight: 400; + color: #fff; +} +h3 { + font-size: 125%; + font-weight: 300; + color: #fff; +} + +input { + background-color: #444; + border: none; +} + +.dashboard_main { + margin: 0px auto; +} + +.gridster { + margin: 0px auto; +} + +.icon-background { + pointer-events: none; + width: 100%!important; + height: 100%; + position: absolute; + left: 0; + top: 0; + opacity: 0.1; + font-size: 1375%; + text-align: center; + margin-top: 82px; +} + +.list-nostyle { + list-style: none; +} + +.gridster ul { + list-style: none; +} + +.gs-w { + width: 100%; + display: table; + cursor: pointer; + z-index: auto !important; +} + +.iframe { + position: relative; + overflow: hidden; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px;} + +.widget { + padding: 0px 0px; + text-align: center; + width: 100%; + display: table-cell; + vertical-align: middle; + background-color: #444444; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px;} + +.title { + color: #fff; +} + +.icon-inactive { + color: #888; +} + +.icon-active { + color: #aaff00; +} + + +#container { + padding-top: 0px; +} + +.modalDialog { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0,0,0,0.8); + z-index: 9999; + opacity:0; + -webkit-transition: opacity 400ms ease-in; + -moz-transition: opacity 400ms ease-in; + transition: opacity 400ms ease-in; + pointer-events: none; +} + +.modalDialogOpen { + opacity:0.95; + pointer-events: auto; +} + +.modalDialogClose { + opacity:0; + pointer-events: none; +} + +.modalDialog > div { + width: 275px; + position: relative; + margin: 3% auto; + padding: 5px 20px 13px 20px; + border-radius: 10px; +} + +.modalDialogCloseButton { + line-height: 50px; + position: absolute; + right: -25px; + text-align: center; + top: -20px; + width: 50px; + text-decoration: none; + font-weight: bold; + -webkit-border-radius: 25px; + -moz-border-radius: 25px; + border-radius: 25px; +} + +.modalDialogCloseButton:hover { background: #444; } + +.widget-basedisplay-default-label .unit { + font-size: 225%; + font-weight: 400; + display: inline-block; + vertical-align: top; + margin-left: 5px; + margin-top: 5px; +} + +.widget-basedisplay-default-label .value { + display: inline-block; + vertical-align: middle; +} + +.widget-basedisplay-default-label .valueunit { + width: 100%; + vertical-align: middle; +} + +.widget-basedisplay-default-label .title { + position: absolute; + top: 5px; + width: 100%; +} + +.widget-basedisplay-default-label .title2 { + position: absolute; + top: 23px; + width: 100%; +} + +.widget-basedisplay-default-label .state_text { + position: absolute; + bottom: -3px; + width: 100%; +} diff --git a/ha/appdaemon_config/compiled/html/default/hello_body.html b/ha/appdaemon_config/compiled/html/default/hello_body.html new file mode 100644 index 0000000..20458d0 --- /dev/null +++ b/ha/appdaemon_config/compiled/html/default/hello_body.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/ha/appdaemon_config/compiled/html/default/hello_head.html b/ha/appdaemon_config/compiled/html/default/hello_head.html new file mode 100644 index 0000000..f3d270b --- /dev/null +++ b/ha/appdaemon_config/compiled/html/default/hello_head.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/ha/appdaemon_config/compiled/javascript/application.js b/ha/appdaemon_config/compiled/javascript/application.js new file mode 100644 index 0000000..3d02a7e --- /dev/null +++ b/ha/appdaemon_config/compiled/javascript/application.js @@ -0,0 +1,3117 @@ +function baseentitypicture(widget_id, url, skin, parameters) +{ + self = this + + // Initialization + + self.parameters = parameters; + + var callbacks = [] + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ]; + + if ("base_url" in parameters && parameters.base_url != "") { + self.base_url = parameters.base_url; + }else{ + self.base_url = ""; + } + + // Call the parent constructor to get things moving + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + function OnStateAvailable(self, state) + { + set_view(self, state) + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + set_view(self, state) + } + + function set_view(self, state) + { + if("entity_picture" in state.attributes){ + self.set_field(self, "img_inernal_src", self.base_url + state.attributes["entity_picture"]); + self.set_field(self, "img_internal_style", ""); + }else{ + self.set_field(self, "img_inernal_src", ""); + self.set_field(self, "img_internal_style", "display: none;"); + } + } +} + +function baseheater(widget_id, url, skin, parameters) +{ + self = this + self.widget_id = widget_id + self.parameters = parameters + + if ("monitored_entity" in self.parameters) + { + entity = self.parameters.monitored_entity + } + else + { + icon_entity = self.parameters.icon_entity + slider_entity = self.parameters.slider_entity + } + + + self.onChange = onChange + self.OnButtonClick = OnButtonClick + + var callbacks = [ + {"selector": '#' + widget_id + ' > span', "action": "click", "callback": self.OnButtonClick}, + {"observable": "Temperature", "action": "change", "callback": self.onChange}, + ] + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("icon_entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.icon_entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}, + {"entity": parameters.slider_entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}, + ] + } + else + { + var monitored_entities = [] + } + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + + function OnStateAvailable(self, state) + { + if ("min" in state.attributes) + { + self.minvalue = state.attributes.min + self.maxvalue = state.attributes.max + self.stepvalue = state.attributes.step + self.thermovalue = state.state + set_options(self, self.minvalue, self.maxvalue, self.stepvalue, self.thermovalue) + } + else + { + self.state = state.state + set_iconview(self, self.state) + } + } + + function OnStateUpdate(self, state) + { + if ("min" in state.attributes) + { + self.thermovalue = state.state + set_sliderview(self, self.thermovalue) + } + else + { + self.state = state.state + set_iconview(self, self.state) + } + } + + function OnButtonClick(self) + { + if (self.state == "off") + { + args = self.parameters.post_service_active + } + else + { + args = self.parameters.post_service_inactive + } + //alert(args) + self.call_service(self, args) + toggle(self) + } + + function onChange(self, state) + { + if (self.thermovalue != self.ViewModel.Temperature()) + { + self.thermovalue = self.ViewModel.Temperature() + args = self.parameters.post_service_slider_change + args["value"] = self.thermovalue + self.call_service(self, args) + } + } + + function toggle(self) + { + if (self.state == "on") + { + self.state = "off"; + } + else + { + self.state = "on"; + } + set_iconview(self, self.state) + } + + function set_options(self, minvalue, maxvalue, stepvalue, state) + { + self.set_field(self, "MinValue", minvalue) + self.set_field(self, "MaxValue", maxvalue) + self.set_field(self, "StepValue", stepvalue) + self.set_field(self, "Temperature", state) + } + + function set_iconview(self, state) + { + if (state == "on") + { + self.set_icon(self, "icon", self.icons.icon_on) + self.set_field(self, "icon_style", self.css.icon_style_active) + } + else + { + self.set_icon(self, "icon", self.icons.icon_off) + self.set_field(self, "icon_style", self.css.icon_style_inactive) + } + } + + function set_sliderview(self, state) + { + if (typeof state == 'undefined') + { + self.set_field(self, "Temperature", 0) + } + else + { + self.set_field(self, "Temperature", state) + } + } + +} + +function baseweather(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Parameters may come in useful later on + + self.parameters = parameters; + + var callbacks = []; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + // Map will be used to know what field are we going to update from what sensor + self.entities_map = {} + + var monitored_entities = [] + + var entities = $.extend({}, parameters.entities, parameters.sensors); + for (var key in entities) + { + var entity = entities[key] + if (entity != '' && check_if_forecast_sensor(parameters.show_forecast, entity)) + { + monitored_entities.push({ + "entity": entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate + }) + self.entities_map[entity] = key + } + } + + // If forecast is disabled - don't monitor the forecast sensors + function check_if_forecast_sensor(show_forecast, entity) + { + if (show_forecast) + { + return true + } + else if(entity.substring(entity.length - 2) === "_1") + { + return false + } + else + { + return true + } + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The OnStateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateUpdate(self, state) + { + set_view(self, state) + } + + function OnStateAvailable(self, state) + { + field = self.entities_map[state.entity_id] + if (field == 'temperature') + { + self.set_field(self, "unit", state.attributes.unit_of_measurement) + } + else if (field == 'wind_speed') + { + self.set_field(self, "wind_unit", state.attributes.unit_of_measurement) + } + else if (field == 'pressure') + { + self.set_field(self, "pressure_unit", state.attributes.unit_of_measurement) + } + else if (field == 'precip_intensity') + { + self.set_field(self, "rain_unit", state.attributes.unit_of_measurement) + } + set_view(self, state) + } + + function set_view(self, state) + { + field = self.entities_map[state.entity_id] + if (field) + { + if (field == 'icon' || field == 'forecast_icon') + { + self.set_field(self, field, state.state) + return + } + + if (field == 'precip_type') + { + self.set_field(self, "precip_type_icon", self.parameters.icons[state.state]) + } + else if (field == 'forecast_precip_type') + { + self.set_field(self, "forecast_precip_type_icon", self.parameters.icons[state.state]) + } + else if (field == 'wind_bearing') + { + var counts = [45, 90, 135, 180, 225, 270, 315] + var goal = (parseInt(state.state) + 270) % 360 + var closest = counts.reduce(function(prev, curr) { + return (Math.abs(curr - goal) < Math.abs(prev - goal) ? curr : prev); + }); + self.set_field(self, "bearing_icon", "mdi-rotate-" + closest) + } + self.set_field(self, field, self.format_number(self, state.state)) + } + } +} + +function basealarm(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Parameters may come in useful later on + + self.parameters = parameters + + self.OnButtonClick = OnButtonClick + self.OnCloseClick = OnCloseClick + self.OnDigitClick = OnDigitClick + self.OnArmHomeClick = OnArmHomeClick + self.OnArmAwayClick = OnArmAwayClick + self.OnDisarmClick = OnDisarmClick + self.OnTriggerClick = OnTriggerClick + + + var callbacks = + [ + {"selector": '#' + widget_id + ' > span', "action": "click", "callback": self.OnButtonClick}, + {"selector": '#' + widget_id + ' #close', "action": "click", "callback": self.OnCloseClick}, + {"selector": '#' + widget_id + ' #0', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "0"}}, + {"selector": '#' + widget_id + ' #1', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "1"}}, + {"selector": '#' + widget_id + ' #2', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "2"}}, + {"selector": '#' + widget_id + ' #3', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "3"}}, + {"selector": '#' + widget_id + ' #4', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "4"}}, + {"selector": '#' + widget_id + ' #5', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "5"}}, + {"selector": '#' + widget_id + ' #6', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "6"}}, + {"selector": '#' + widget_id + ' #7', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "7"}}, + {"selector": '#' + widget_id + ' #8', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "8"}}, + {"selector": '#' + widget_id + ' #9', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "9"}}, + {"selector": '#' + widget_id + ' #BS', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "BS"}}, + {"selector": '#' + widget_id + ' #AH', "action": "click", "callback": self.OnArmHomeClick}, + {"selector": '#' + widget_id + ' #AA', "action": "click", "callback": self.OnArmAwayClick}, + {"selector": '#' + widget_id + ' #DA', "action": "click", "callback": self.OnDisarmClick}, + {"selector": '#' + widget_id + ' #TR', "action": "click", "callback": self.OnTriggerClick}, + + ] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + self.set_view = set_view + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + self.set_field(self, "state", self.map_state(self, state.state)) + } + + function OnStateUpdate(self, state) + { + self.set_field(self, "state", self.map_state(self, state.state)) + } + + function OnButtonClick(self) + { + self.code = self.parameters.initial_string + self.set_view(self) + + $('#' + widget_id + ' > #Dialog').removeClass("modalDialogClose") + $('#' + widget_id + ' > #Dialog').addClass("modalDialogOpen") + } + + function OnCloseClick(self) + { + $('#' + widget_id + ' > #Dialog').removeClass("modalDialogOpen") + $('#' + widget_id + ' > #Dialog').addClass("modalDialogClose") + } + + function OnDigitClick(self, parameters) + { + if (parameters.digit == "BS") + { + if (self.code != self.parameters.initial_string) + { + if (self.code.length == 1) + { + self.code = self.parameters.initial_string + } + else + { + self.code = self.code.substring(0, self.code.length - 1); + } + } + } + else + { + if (self.code == self.parameters.initial_string) + { + self.code = parameters.digit + } + else + { + self.code = self.code + parameters.digit + } + } + self.set_view(self) + } + + function OnArmHomeClick(self) + { + + args = self.parameters.post_service_ah + args["code"] = self.code + self.call_service(self, args) + + self.code = self.parameters.initial_string + self.set_view(self) + } + + function OnArmAwayClick(self) + { + args = self.parameters.post_service_aa + args["code"] = self.code + self.call_service(self, args) + + self.code = self.parameters.initial_string + self.set_view(self) + } + + function OnDisarmClick(self) + { + args = self.parameters.post_service_da + args["code"] = self.code + self.call_service(self, args) + + self.code = self.parameters.initial_string + self.set_view(self) + } + + function OnTriggerClick(self) + { + args = self.parameters.post_service_tr + args["code"] = self.code + self.call_service(self, args) + + self.code = self.parameters.initial_string + self.set_view(self) + } + + function set_view(self) + { + self.set_field(self, "code", self.code) + } +} + +function baseerror(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters + + var callbacks = [] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + var monitored_entities = [] + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + +} + +function basetemperature(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters + + var callbacks = [] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + drawChart(self, state) + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + function set_value(self, state) + { + self.gauge.value = state.state + //self.gauge.update() + } + function drawChart(self, state) + { + self.gauge = new LinearGauge({ + renderTo: document.getElementById(self.widget_id).getElementsByClassName('gaugeclass')[0], + type: 'linear-gauge', + width: '120', + height: '120', + valueInt: 2, + valueDec: 1, + colorTitle: '#333', + minValue: 17, + maxValue: 25, + //majorTicks: [0, 5, 10, 15, 20, 25, 30, 35], + minorTicks: 2, + strokeTicks: true + }); + // self.gauge.value = state.state + // update so the red bar is current after a dashboard compile + + if ( null == self.parameters.settings.value ){ + self.parameters.settings.value = state.state + } + self.gauge.update(self.parameters.settings) + } +} + +function baserss(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters; + + // + // RSS Info is always in the admin namespace + // + self.parameters.namespace = "admin"; + + var callbacks = []; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + function set_value(self, state) + { + self.story = 0; + clearTimeout(self.timer); + show_next_story(self); + self.timer = setInterval(show_next_story, self.parameters.interval * 1000, self); + } + + function show_next_story(self) + { + var stories = self.entity_state[parameters.entity].state.feed.entries; + self.set_field(self, "text", stories[self.story].title); + if ("show_description" in self.parameters && self.parameters.show_description === 1) + { + if ("summary" in stories[self.story]) + { + self.set_field(self, "description", stories[self.story].summary) + } + if ("description" in stories[self.story]) + { + self.set_field(self, "description", stories[self.story].description) + } + } + self.story = self.story + 1; + if ((self.story >= stories.length) || ("recent" in parameters && self.story >= parameters.recent)) + { + self.story = 0; + } + } +} + +function basetext(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters; + + self.OnChange = OnChange; + + var callbacks = [ + {"observable": "TextValue", "action": "change", "callback": self.OnChange}, + ]; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnChange(self, state) + { + if (self.state != self.ViewModel.TextValue()) + { + self.state = self.ViewModel.TextValue() + args = self.parameters.post_service + args["value"] = self.state + self.call_service(self, args) + } + + } + + function OnStateAvailable(self, state) + { + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + function set_value(self, state) + { + value = self.map_state(self, state.state) + self.set_field(self, "TextValue", value) + } + +} + +function basemedia(widget_id, url, skin, parameters) +{ + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Parameters may come in useful later on + + self.parameters = parameters; + + self.OnPlayButtonClick = OnPlayButtonClick; + self.OnPreviousButtonClick = OnPreviousButtonClick; + self.OnNextButtonClick = OnNextButtonClick; + self.OnRaiseLevelClick = OnRaiseLevelClick; + self.OnLowerLevelClick = OnLowerLevelClick; + + self.min_level = 0; + self.max_level = 1; + + if ("step" in self.parameters) + { + self.step = self.parameters.step / 100; + } + else + { + self.step = 0.1; + } + + var callbacks = + [ + {"selector": '#' + widget_id + ' #play', "action": "click", "callback": self.OnPlayButtonClick}, + {"selector": '#' + widget_id + ' #level-up', "action": "click", "callback": self.OnRaiseLevelClick}, + {"selector": '#' + widget_id + ' #level-down', "action": "click", "callback": self.OnLowerLevelClick}, + {"selector": '#' + widget_id + ' #previous', "action": "click", "callback": self.OnPreviousButtonClick}, + {"selector": '#' + widget_id + ' #next', "action": "click", "callback": self.OnNextButtonClick} + ]; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ]; + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + function OnStateAvailable(self, state) + { + self.entity = state.entity_id; + self.level = state.attributes.volume_level; + self.state = state; + set_view(self, state) + if ("dump_capabilities" in self.parameters && self.parameters["dump_capabilities"] == "1") + { + display_supported_functions(self) + } + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + self.level = state.attributes.volume_level; + self.state = state; + set_view(self, state) + } + + function OnPlayButtonClick(self) + { + if (self.entity_state[self.entity].state !== "playing") + { + if (is_supported(self, "PLAY_MEDIA")) + { + args = self.parameters.post_service_play_pause; + self.call_service(self, args) + } + else + { + console.log("Play attribute not supported") + } + } + else + { + if (is_supported(self, "PAUSE")) + { + args = self.parameters.post_service_pause; + self.call_service(self, args) + } + else if (is_supported(self, "STOP")) + { + args = self.parameters.post_service_stop; + self.call_service(self, args) + } + else + { + // Try Play/Pause + args = self.parameters.post_service_play_pause; + self.call_service(self, args) + } + } + } + + function OnPreviousButtonClick(self) + { + if (is_supported(self, "PREVIOUS_TRACK")) + { + args = self.parameters.post_service_previous; + self.call_service(self, args) + } + else + { + console.log("NEXT_TRACK attribute not supported") + } + } + + function OnNextButtonClick(self) + { + if (is_supported(self, "NEXT_TRACK")) + { + args = self.parameters.post_service_next; + self.call_service(self, args) + } + else + { + console.log("NEXT_TRACK attribute not supported") + } + } + + + + function OnRaiseLevelClick(self) + { + self.level = Math.round((self.level + self.step) * 100) / 100; + if (self.level > self.max_level) + { + self.level = self.max_level + } + + args = self.parameters.post_service_level; + args["volume_level"] = self.level; + self.call_service(self, args) + + } + + function OnLowerLevelClick(self) + { + self.level = Math.round((self.level - self.step) * 100) / 100; + if (self.level < self.min_level) + { + self.level = self.min_level + } + + args = self.parameters.post_service_level; + args["volume_level"] = self.level; + self.call_service(self, args) + + + } + + function set_view(self, state) + { + if (state.state === "playing") + { + self.set_field(self, "play_icon_style", self.css.icon_style_active) + self.set_icon(self, "play_icon", self.icons.pause_icon) + } + else + { + self.set_field(self, "play_icon_style", self.css.icon_style_inactive) + self.set_icon(self, "play_icon", self.icons.play_icon) + } + + if ("media_artist" in state.attributes) + { + self.set_field(self, "artist", state.attributes.media_artist); + } + + if ("media_album_name" in state.attributes) + { + self.set_field(self, "album", state.attributes.media_album_name) + } + if ("media_title" in state.attributes) + { + if ("truncate_name" in self.parameters) + { + name = state.attributes.media_title.substring(0, self.parameters.truncate_name); + } + else + { + name = state.attributes.media_title + } + self.set_field(self, "media_title", name); + } + if ("volume_level" in state.attributes) + { + self.set_field(self, "level", Math.round(state.attributes.volume_level * 100)) + } + else + { + self.set_field(self, "level", 0) + } + + } + + function is_supported(self, attr) + { + var support = + { + "PAUSE": 1, + "SEEK": 2, + "VOLUME_SET": 4, + "VOLUME_MUTE": 8, + "PREVIOUS_TRACK": 16, + "NEXT_TRACK": 32, + "TURN_ON": 128, + "TURN_OFF": 256, + "PLAY_MEDIA": 512, + "VOLUME_STEP": 1024, + "SELECT_SOURCE": 2048, + "STOP": 4096, + "CLEAR_PLAYLIST": 8192, + "PLAY": 16384, + "SHUFFLE_SET": 32768 + }; + + var supported = self.state.attributes.supported_features; + + if (attr in support) + { + var attr_value = support[attr]; + if ((supported & attr_value) == attr_value) + { + return true + } + else + { + return false + } + } + else + { + console.log("Unknown media player attribute: " + attr) + return false + } + } + + function display_supported_functions(self) + { + console.log(self.parameters.entity); + console.log("Supported Features: " + self.state.attributes.supported_features); + console.log("PAUSE: " + is_supported(self, "PAUSE")) + console.log("SEEK: " + is_supported(self, "SEEK")) + console.log("VOLUME_SET: " + is_supported(self, "VOLUME_SET")) + console.log("VOLUME_MUTE: " + is_supported(self, "VOLUME_MUTE")) + console.log("PREVIOUS_TRACK: " + is_supported(self, "PREVIOUS_TRACK")) + console.log("NEXT_TRACK: " + is_supported(self, "NEXT_TRACK")) + console.log("TURN_ON: " + is_supported(self, "TURN_ON")) + console.log("TURN_OFF: " + is_supported(self, "TURN_OFF")) + console.log("PLAY_MEDIA: " + is_supported(self, "PLAY_MEDIA")) + console.log("VOLUME_STEP: " + is_supported(self, "VOLUME_STEP")) + console.log("SELECT_SOURCE: " + is_supported(self, "SELECT_SOURCE")) + console.log("STOP: " + is_supported(self, "STOP")) + console.log("CLEAR_PLAYLIST: " + is_supported(self, "CLEAR_PLAYLIST")) + console.log("PLAY: " + is_supported(self, "PLAY")) + console.log("SHUFFLE_SET: " + is_supported(self, "SHUFFLE_SET")) + } +} + +function baseicon(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Parameters may come in useful later on + + self.parameters = parameters; + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + self.OnIconClick = OnIconClick; + + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ]; + + var callbacks = [ + {"selector": '#' + widget_id, "action": "click", "callback": self.OnIconClick}, + ] + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + function OnStateAvailable(self, state) + { + self.state = state.state; + set_view(self, self.state) + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + self.state = state.state; + + delay_time = self.parameters.update_delay; + if (delay_time !== undefined) { + if (self.timeout !== undefined) { + clearTimeout(self.timeout); + } + + self.timeout = setTimeout(function() {set_view(self, self.state)}, delay_time * 1000); + } else { + set_view(self, self.state); + } + } + + function OnIconClick(self) { + if (self.post_service_active !== undefined) { + self.call_service(self, self.post_service_active); + } + } + + // Set view is a helper function to set all aspects of the widget to its + // current state - it is called by widget code when an update occurs + // or some other event that requires a an update of the view + + function set_view(self, state, level) + { + if ("icons" in self.parameters) + { + if (state in self.parameters.icons) + { + self.set_icon(self, "icon", self.parameters.icons[state].icon); + self.set_field(self, "icon_style", self.parameters.icons[state].style); + set_service_call(self, self.parameters.icons[state]); + } + else if ("default" in self.parameters.icons) + { + self.set_icon(self, "icon", self.parameters.icons.default.icon); + self.set_field(self, "icon_style", self.parameters.icons.default.style); + set_service_call(self, self.parameters.default); + } + else + { + self.set_icon(self, "icon", "fa-circle-thin"); + self.set_field(self, "icon_style", "color: white"); + set_service_call(self, {}); + } + + } + + if ("state_text" in self.parameters && self.parameters.state_text == 1) + { + self.set_field(self, "state_text", self.map_state(self, state)) + } + } + + function set_service_call(self, data) { + if (data !== undefined) { + if (data.post_service_active !== undefined) { + self.post_service_active = data.post_service_active; + if (self.post_service_active["namespace"] === undefined) { + self.post_service_active["namespace"] = self.parameters.namespace; + } + } else { + self.post_service_active = undefined; + } + } + } +} + +function baseswitch(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters; + + // Toggle needs to be referenced from self for the timeout function + + self.toggle = toggle; + + // Define callbacks for on click events + // They are defined as functions below and can be any name as long as the + // 'self'variables match the callbacks array below + // We need to add them into the object for later reference + + self.OnButtonClick = OnButtonClick; + + if ("enable" in self.parameters && self.parameters.enable === 1) + { + var callbacks = + [ + {"selector": '#' + widget_id + ' > span', "action": "click", "callback": self.OnButtonClick}, + ] + } + else + { + var callbacks = [] + } + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}, + ]; + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + function OnStateAvailable(self, state) + { + self.state = state.state; + set_view(self, self.state) + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + if (!("ignore_state" in self.parameters) || self.parameters.ignore_state === 0) + { + self.state = state.state; + set_view(self, self.state) + } + } + + function OnButtonClick(self) + { + if (self.state === self.parameters.state_active) + { + args = self.parameters.post_service_inactive + } + else + { + args = self.parameters.post_service_active + } + args["namespace"] = self.parameters.namespace + + self.call_service(self, args); + toggle(self); + if ("momentary" in self.parameters) + { + setTimeout(function() { self.toggle(self) }, self.parameters["momentary"]) + } + } + + function toggle(self) + { + if (self.state === self.parameters.state_active) + { + self.state = self.parameters.state_inactive; + } + else + { + self.state = self.parameters.state_active; + } + set_view(self, self.state) + } + + // Set view is a helper function to set all aspects of the widget to its + // current state - it is called by widget code when an update occurs + // or some other event that requires a an update of the view + + function set_view(self, state, level) + { + if (state === self.parameters.state_active || ("active_map" in self.parameters && self.parameters.active_map.includes(state))) + { + self.set_icon(self, "icon", self.icons.icon_on); + self.set_field(self, "icon_style", self.css.icon_style_active) + } + else + { + self.set_icon(self, "icon", self.icons.icon_off); + self.set_field(self, "icon_style", self.css.icon_style_inactive) + } + if ("state_text" in self.parameters && self.parameters.state_text === 1) + { + self.set_field(self, "state_text", self.map_state(self, state)) + } + } +} + +function baseclock(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Parameters may come in useful later on + + self.parameters = parameters + + // Define callbacks for on click events + // They are defined as functions below and can be any name as long as the + // 'self'variables match the callbacks array below + // We need to add them into the object for later reference + + var callbacks = [] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + var monitored_entities = [] + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + updateTime(self) + + setInterval(updateTime, 500, self); + + function updateTime(self) + { + var today = new Date(); + h = today.getHours(); + m = today.getMinutes(); + s = today.getSeconds(); + m = formatTime(m); + + if ("date_format_country" in self.parameters) + { + if ("date_format_options" in self.parameters) + { + self.set_field(self, "date", today.toLocaleDateString(self.parameters.date_format_country, self.parameters.date_format_options)); + } + else + { + self.set_field(self, "date", today.toLocaleDateString(self.parameters.date_format_country)); + } + } + else + { + self.set_field(self, "date", today.toLocaleDateString()); + } + + if ("time_format" in self.parameters && self.parameters.time_format == "24hr") + { + time = h + ":" + m; + pm = "" + } + else + { + time = formatHours(h) + ":" + m; + pm = " " + formatAmPm(h) + } + + if ("show_seconds" in self.parameters && self.parameters.show_seconds == 1) + { + time = time + ":" + formatTime(s) + } + + time = time + pm + self.set_field(self, "time", time); + } + + function formatTime(i) + { + if (i < 10 ) + { + return "0" + i; + } + else + { + return i; + } + } + + function formatAmPm(h) + { + if (h >= 12) + { + return "PM"; + } + else + { + return "AM"; + } + } + + function formatHours(h) + { + if (h > 12) + { + return h - 12; + } + else if (h == 0) + { + return 12; + } + else + { + return h; + } + } +} + +function basecamera(widget_id, url, skin, parameters) +{ + self = this + + // Initialization + + self.parameters = parameters; + + var callbacks = [] + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}, + ]; + + // Call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Set the url + + self.index = 0; + refresh_frame(self) + self.timeout = undefined + + function refresh_frame(self) + { + if ("base_url" in self.parameters && "access_token" in self) { + var endpoint = '/api/camera_proxy/' + if ('stream' in self.parameters && self.parameters.stream) { + endpoint = '/api/camera_proxy_stream/' + } + + var url = self.parameters.base_url + endpoint + self.parameters.entity + '?token=' + self.access_token + } + else + { + var url = '/images/Blank.gif' + } + + if (url.indexOf('?') > -1) + { + url = url + "&time=" + Math.floor((new Date).getTime()/1000); + } + else + { + url = url + "?time=" + Math.floor((new Date).getTime()/1000); + } + self.set_field(self, "img_src", url); + self.index = 0 + + var refresh = 10 + if ('stream' in self.parameters && self.parameters.stream == "on") { + refresh = 0 + } + if ("refresh" in self.parameters) + { + refresh = self.parameters.refresh + } + + if (refresh > 0) + { + clearTimeout(self.timeout) + self.timeout = setTimeout(function() {refresh_frame(self)}, refresh * 1000); + } + + } + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + function OnStateAvailable(self, state) + { + self.state = state.state; + self.access_token = state.attributes.access_token + refresh_frame(self) + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + self.state = state.state; + self.access_token = state.attributes.access_token + refresh_frame(self) + } + + } + +function baseradial(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters + + var callbacks = [] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + activateChart(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + function set_value(self, state) + { + self.gauge.value = state.state + // self.gauge.update() + } + + function activateChart(self, state) { + self.gauge = new RadialGauge({ + renderTo: document.getElementById(self.widget_id).getElementsByClassName('gaugeclass')[0], + type: 'radial-gauge', + width: '120', + height: '120', + //valueInt: 2, + //valueDec: 1, + colorTitle: '#333', + //minValue: 17, + //maxValue: 25, + //minorTicks: 2, + //strokeTicks: true, + }) + self.gauge.value = state.state + self.gauge.update(self.parameters.settings) + //self.gauge.draw() + } +} + +function baselight(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Parameters may come in useful later on + + self.parameters = parameters + + // Parameter handling + + if ("monitored_entity" in self.parameters) + { + entity = self.parameters.monitored_entity + } + else + { + entity = self.parameters.entity + } + + if ("on_brightness" in self.parameters) + { + self.on_brightness = self.parameters.on_brightness + } + else + { + self.on_brightness = 127 + } + + // Define callbacks for on click events + // They are defined as functions below and can be any name as long as the + // 'self'variables match the callbacks array below + // We need to add them into the object for later reference + + self.OnButtonClick = OnButtonClick + self.OnRaiseLevelClick = OnRaiseLevelClick + self.OnLowerLevelClick = OnLowerLevelClick + + var callbacks = + [ + {"selector": '#' + widget_id + ' > span', "action": "click", "callback": self.OnButtonClick}, + {"selector": '#' + widget_id + ' #level-up', "action": "click", "callback": self.OnRaiseLevelClick}, + {"selector": '#' + widget_id + ' #level-down', "action": "click", "callback": self.OnLowerLevelClick}, + ] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + var monitored_entities = + [ + {"entity": entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + function OnStateAvailable(self, state) + { + self.state = state.state; + if ("brightness" in state.attributes) + { + self.level = state.attributes.brightness + } + else + { + self.level = 0 + } + set_view(self, self.state, self.level) + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + self.state = state.state; + if ("brightness" in state.attributes) + { + self.level = state.attributes.brightness + } + else + { + self.level = 0 + } + + set_view(self, self.state, self.level) + } + + function OnButtonClick(self) + { + if (self.state == "off") + { + args = jQuery.extend(true, {}, self.parameters.post_service_active) + if ("on_attributes" in self.parameters) + { + for (var attr in self.parameters.on_attributes) + { + args[attr] = self.parameters.on_attributes[attr] + } + } + } + else + { + args = jQuery.extend(true, {}, self.parameters.post_service_inactive) + } + self.call_service(self, args) + toggle(self) + } + + function OnRaiseLevelClick(self) + { + self.level = self.level + 255/10; + self.level = parseInt(self.level) + if (self.level > 255) + { + self.level = 255 + } + args = jQuery.extend(true, {}, self.parameters.post_service_active); + args["brightness"] = self.level + self.call_service(self, args) + } + + function OnLowerLevelClick(self) + { + self.level = self.level - 255/10; + if (self.level < 0) + { + self.level = 0; + } + self.level = parseInt(self.level) + if (self.level == 0) + { + args = jQuery.extend(true, {}, self.parameters.post_service_inactive) + } + else + { + args = jQuery.extend(true, {}, self.parameters.post_service_active) + args["brightness"] = self.level + } + self.call_service(self, args) + } + + function toggle(self) + { + if (self.state == "on") + { + self.state = "off"; + self.level = 0 + } + else + { + self.state = "on"; + } + set_view(self, self.state, self.level) + } + + // Set view is a helper function to set all aspects of the widget to its + // current state - it is called by widget code when an update occurs + // or some other event that requires a an update of the view + + function set_view(self, state, level) + { + + if (state == "on") + { + // Set Icon will set the style correctly for an icon + self.set_icon(self, "icon", self.icons.icon_on) + // Set view will set the view for the appropriate field + self.set_field(self, "icon_style", self.css.icon_style_active) + } + else + { + self.set_icon(self, "icon", self.icons.icon_off) + self.set_field(self, "icon_style", self.css.icon_style_inactive) + } + if (typeof level == 'undefined') + { + self.set_field(self, "level", 0) + } + else + { + self.set_field(self, "level", Math.ceil((level*100/255) / 10) * 10) + } + } +} + +function basefan(widget_id, url, skin, parameters) +{ + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Parameters may come in useful later on + + self.parameters = parameters; + + self.OnPowerButtonClick = OnPowerButtonClick; + self.On1ButtonClick = On1ButtonClick; + self.On2ButtonClick = On2ButtonClick; + self.On3ButtonClick = On3ButtonClick; + + var callbacks = + [ + + {"selector": '#' + widget_id + ' #power', "action": "click", "callback": self.OnPowerButtonClick}, + {"selector": '#' + widget_id + ' #speed1', "action": "click", "callback": self.On1ButtonClick}, + {"selector": '#' + widget_id + ' #speed2', "action": "click", "callback": self.On2ButtonClick}, + {"selector": '#' + widget_id + ' #speed3', "action": "click", "callback": self.On3ButtonClick} + ]; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ]; + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + function OnStateAvailable(self, state) + { + self.state = state; + set_view(self, state) + } + + // The OnStateUpdate function will be called when the specific entity + // receives a state update - its new values will be available + // in self.state[] and returned in the state parameter + + function OnStateUpdate(self, state) + { + self.state = state ; + set_view(self, state) + } + + function OnPowerButtonClick(self) { + + if (self.state.state=="off"){ + args = self.parameters.post_service_active; + } + else{ + args = self.parameters.post_service_inactive; + } + self.call_service(self, args); + } + + function On1ButtonClick(self) { + args = self.parameters.post_service_speed; + args["speed"] = self.parameters.fields.low_speed; + self.call_service(self, args); + } + function On2ButtonClick(self) { + args = self.parameters.post_service_speed; + args["speed"]= self.parameters.fields.medium_speed; + self.call_service(self, args); + } + function On3ButtonClick(self) { + args = self.parameters.post_service_speed; + args["speed"] = self.parameters.fields.high_speed; + self.call_service(self, args); + } + + + function set_view(self, state) + { + + if (state.state != "on") + { + self.set_icon(self, "icon", self.icons.icon_inactive) + self.set_field(self, "icon_style", self.css.icon_style_inactive) + self.set_field(self,"speed1_style", self.css.speed1_style_inactive) + self.set_field(self,"speed2_style", self.css.speed2_style_inactive) + self.set_field(self,"speed3_style", self.css.speed3_style_inactive) + self.set_icon(self, "icon1", self.icons.icon1_inactive) + self.set_icon(self, "icon2", self.icons.icon2_inactive) + self.set_icon(self, "icon3", self.icons.icon3_inactive) + } + else + //Fan is on + { + //turn main icon on & dispay speed selector + self.set_icon(self, "icon", self.icons.icon_active) + self.set_field(self, "icon_style", self.css.icon_style_active) + + //decide which icon to mark as selected + if (state.attributes.speed == self.parameters.fields.low_speed){ + self.set_field(self,"speed1_style", self.css.speed1_style_active) + self.set_field(self,"speed2_style", self.css.speed2_style_inactive) + self.set_field(self,"speed3_style", self.css.speed3_style_inactive) + self.set_icon(self, "icon1", self.icons.icon1_active) + self.set_icon(self, "icon2", self.icons.icon2_inactive) + self.set_icon(self, "icon3", self.icons.icon3_inactive) + } + else if (state.attributes.speed == self.parameters.fields.medium_speed){ + self.set_field(self,"speed1_style", self.css.speed1_style_inactive) + self.set_field(self,"speed2_style", self.css.speed2_style_active) + self.set_field(self,"speed3_style", self.css.speed3_style_inactive) + self.set_icon(self, "icon1", self.icons.icon1_inactive) + self.set_icon(self, "icon2", self.icons.icon2_active) + self.set_icon(self, "icon3", self.icons.icon3_inactive) + } + else if (state.attributes.speed == self.parameters.fields.high_speed){ + self.set_field(self,"speed1_style", self.css.speed1_style_inactive) + self.set_field(self,"speed2_style", self.css.speed2_style_inactive) + self.set_field(self,"speed3_style", self.css.speed3_style_active) + self.set_icon(self, "icon1", self.icons.icon1_inactive) + self.set_icon(self, "icon2", self.icons.icon2_inactive) + self.set_icon(self, "icon3", self.icons.icon3_active) + } + } + } + +} + +function baseslider(widget_id, url, skin, parameters) +{ + + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Parameters may come in useful later on + + self.parameters = parameters + + self.OnRaiseLevelClick = OnRaiseLevelClick + self.OnLowerLevelClick = OnLowerLevelClick + + var callbacks = + [ + {"selector": '#' + widget_id + ' #level-up', "action": "click", "callback": self.OnRaiseLevelClick}, + {"selector": '#' + widget_id + ' #level-down', "action": "click", "callback": self.OnLowerLevelClick}, + ] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + self.min = state.attributes.min + self.max = state.attributes.max + self.step = state.attributes.step + self.level = state.state + if ("units" in self.parameters) + { + self.set_field(self, "unit", self.parameters.units) + } + set_view(self, state) + } + + function OnStateUpdate(self, state) + { + self.level = state.state + set_view(self, state) + } + + function OnRaiseLevelClick(self) + { + self.level = parseFloat(self.level) + self.step; + if (self.level > self.max) + { + self.level = self.max + } + args = self.parameters.post_service + args["value"] = self.level + self.call_service(self, args) + } + + function OnLowerLevelClick(self, args) + { + self.level = parseFloat(self.level) - self.step; + if (self.level < self.min) + { + self.level = self.min + } + args = self.parameters.post_service + args["value"] = self.level + self.call_service(self, args) + } + + function set_view(self, state) + { + self.set_field(self, "level", self.format_number(self, state.state)) + } +} + +function baseinputnumber(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters + + self.onChange = onChange + + var callbacks = [ + {"observable": "SliderValue", "action": "change", "callback": self.onChange}, + ] + + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + self.state = state.state + self.minvalue = state.attributes.min + self.maxvalue = state.attributes.max + self.stepvalue = state.attributes.step + set_options(self, self.minvalue, self.maxvalue, self.stepvalue, state) + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + self.state = state.state + set_value(self, state) + } + + function set_value(self, state) + { + value = self.map_state(self, state.state) + self.set_field(self, "SliderValue", value) + self.set_field(self, "sliderValue", self.format_number(self,value)) + } + + function onChange(self, state) + { + if (self.state != self.ViewModel.SliderValue()) + { + self.state = self.ViewModel.SliderValue() + args = self.parameters.post_service + args["value"] = self.state + self.call_service(self, args) + } + } + + function set_options(self, minvalue, maxvalue, stepvalue, state) + { + //alert(self.maxvalue) + self.set_field(self, "MinValue", minvalue) + self.set_field(self, "MaxValue", maxvalue) + self.set_field(self, "minValue", self.format_number(self,minvalue)) + self.set_field(self, "maxValue", self.format_number(self,maxvalue)) + self.set_field(self, "StepValue", stepvalue) + } + +} + +function basejavascript(widget_id, url, skin, parameters) +{ + // Store Args + this.widget_id = widget_id + this.parameters = parameters + this.skin = skin + + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters + + // Define callbacks for on click events + // They are defined as functions below and can be any name as long as the + // 'self'variables match the callbacks array below + // We need to add them into the object for later reference + + self.OnButtonClick = OnButtonClick + + var callbacks = + [ + {"selector": '#' + widget_id + ' > span', "action": "click","callback": self.OnButtonClick}, + ] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + var monitored_entities = + [] + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + + + if ("command" in parameters) + { + command = parameters.command + } + else if ("url" in parameters || "dashboard" in parameters) + { + var append = ""; + + if ("url" in parameters) + { + url = parameters.url + } + else + { + url = "/" + parameters.dashboard + if ("forward_parameters" in parameters) + { + append = appendURL(parameters.forward_parameters); + } + } + var i = 0; + + if ("args" in parameters) + { + + url = url + "?"; + + for (var key in parameters.args) + { + if (i != 0) + { + url = url + "&" + } + url = url + key + "=" + parameters.args[key]; + i++ + } + } + if ("skin" in parameters) + { + theskin = parameters.skin + } + else + { + theskin = skin + } + + if (i == 0) + { + url = url + "?skin=" + theskin; + i++ + } + else + { + url = url + "&skin=" + theskin; + i++ + } + + if ("sticky" in parameters) + { + if (i == 0) + { + url = url + "?sticky=" + parameters.sticky; + i++ + } + else + { + url = url + "&sticky=" + parameters.sticky; + i++ + } + } + + if ("return" in parameters) + { + if (i == 0) + { + url = url + "?return=" + parameters.return; + i++ + } + else + { + url = url + "&return=" + parameters.return; + i++ + } + } + else + { + if ("timeout" in parameters) + { + current_dash = location.pathname.split('/')[1]; + if (current_dash.length > 0) + { + if (i == 0) + { + url = url + "?return=" + current_dash; + i++ + } + else + { + url = url + "&return=" + current_dash; + i++ + } + } + } + } + if ("timeout" in parameters) + { + if (i == 0) + { + url = url + "?timeout=" + parameters.timeout; + i++ + } + else + { + url = url + "&timeout=" + parameters.timeout; + i++ + } + } + if ( append != "" ) + { + if (i == 0) + { + url = url + "?" + append; + i++ + } + else + { + url = url + "&" + append; + i++ + } + } + + command = "window.location.href = '" + url + "'" + } + + self.set_icon(self, "icon", self.icons.icon_inactive); + self.set_field(self, "icon_style", self.css.icon_inactive_style); + + self.command = command; + + function appendURL(forward_list) + { + var append = ""; + if (location.search != "") + { + var query = location.search.substr(1); + var result = {}; + query.split("&").forEach(function(part) { + var item = part.split("="); + result[item[0]] = decodeURIComponent(item[1]); + }); + + var useand = false; + for (arg in result) + { + if (arg != "timeout" && arg != "return" && arg != "sticky" && arg != "skin" && + (forward_list.includes(arg) || forward_list.includes("all")) ) + { + if (useand) + { + append += "&"; + } + useand = true; + append += arg + if (result[arg] != "undefined" ) + { + append += "=" + result[arg] + } + } + } + } + return append + } + + function OnButtonClick(self) + { + self.set_icon(self, "icon", self.icons.icon_active); + self.set_field(self, "icon_style", self.css.icon_active_style); + eval(self.command); + } +} + +function baseclimate(widget_id, url, skin, parameters) +{ + + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Parameters may come in useful later on + + self.parameters = parameters + + self.OnRaiseLevelClick = OnRaiseLevelClick + self.OnLowerLevelClick = OnLowerLevelClick + + var callbacks = + [ + {"selector": '#' + widget_id + ' #level-up', "action": "click", "callback": self.OnRaiseLevelClick}, + {"selector": '#' + widget_id + ' #level-down', "action": "click", "callback": self.OnLowerLevelClick}, + ] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + + if( "step" in parameters && ! isNaN(self.parameters.step)) + { + self.step = parseFloat(parameters.step) + } + else + { + self.step = 1 + } + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + self.min = state.attributes.min_temp + self.max = state.attributes.max_temp + self.level = state.attributes.temperature + if ("units" in self.parameters) + { + self.set_field(self, "units", self.parameters.units) + } + else + { + self.set_field(self, "units", state.attributes["unit_of_measurement"]) + } + set_view(self, state) + } + + function OnStateUpdate(self, state) + { + self.level = state.attributes.temperature + set_view(self, state) + } + + function OnRaiseLevelClick(self) + { + self.level = parseFloat(self.level) + self.step; + if (self.level > self.max) + { + self.level = self.max + } + args = self.parameters.post_service + args["temperature"] = self.level + self.call_service(self, args) + } + + function OnLowerLevelClick(self, args) + { + self.level = parseFloat(self.level) - self.step; + if (self.level < self.min) + { + self.level = self.min + } + args = self.parameters.post_service; + args["temperature"] = self.level; + self.call_service(self, args) + } + + function set_view(self, state) + { + self.set_field(self, "level", self.format_number(self, state.attributes.current_temperature)); + if ("temperature" in state.attributes && state.attributes.temperature != null) + { + self.set_field(self, "level2", self.format_number(self, state.attributes.temperature)) + } + else + { + self.set_field(self, "level2", "auto") + } + } +} + +function basegauge(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this + + // Initialization + + self.widget_id = widget_id + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters + + var callbacks = [] + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable + self.OnStateUpdate = OnStateUpdate + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + + self.gauge = new JustGage({ + parentNode: $('#' + widget_id + ' > div')[0], + //id: "graph", + value: 0, + nogradient: true, + levelColors: [self.parameters.low_color, self.parameters.med_color, self.parameters.high_color], + labelFontColor: self.parameters.color, + valueFontColor: self.parameters.color, + levelColorsGradient: false, + gaugeColor: self.parameters.bgcolor, + symbol: self.parameters.units, + min: self.parameters.min, + max: self.parameters.max, + }); + + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks) + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + function set_value(self, state) + { + self.gauge.refresh(state.state) + } +} + +function basedatetime(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters; + + self.OnChange = OnChange; + + var callbacks = [ + {"observable": "DateValue", "action": "change", "callback": self.OnChange}, + {"observable": "TimeValue", "action": "change", "callback": self.OnChange}, + ]; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnChange(self, state) + { + date = self.ViewModel.DateValue() + time = self.ViewModel.TimeValue() + args = self.parameters.post_service + if (self.has_date && self.has_time) { + args["datetime"] = self.state + datetime = new Date(self.state); + args["date"] = date; + args["time"] = time; + } + else if (self.has_date) { + args["date"] = date; + } + else { + args["time"] = time; + } + self.call_service(self, args); + } + + function OnStateAvailable(self, state) + { + self.has_date = state.attributes.has_date + self.has_time = state.attributes.has_time + fields = document.getElementById(self.widget_id).childNodes[2]; + datefield = document.getElementById(self.widget_id).childNodes[2].childNodes[0]; + timefield = document.getElementById(self.widget_id).childNodes[2].childNodes[1]; + if(self.has_date && self.has_time) + { + // do nothing + } + else if(self.has_time) + { + fields.removeChild(datefield) + } + else if(self.has_date) + { + fields.removeChild(timefield) + } + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + + function set_value(self, state) + { + datetime = new Date(state.state); + if (self.has_date && self.has_time) + { + datevalue = datetime.getFullYear() + "-" + pad(datetime.getMonth()+1) + "-" + pad(datetime.getDate()); + timevalue = pad(datetime.getHours()) + ":" + pad(datetime.getMinutes()) + ":" + pad(datetime.getSeconds()); + self.set_field(self, "TimeValue", timevalue); + self.set_field(self, "DateValue", datevalue); + } + else if (self.has_date) + { + datevalue = datetime.getFullYear() + "-" + pad(datetime.getMonth()+1) + "-" + pad(datetime.getDate()); + self.set_field(self, "DateValue", datevalue); + } + else + { + timevalue = pad(datetime.getHours()) + ":" + pad(datetime.getMinutes()) + ":" + pad(datetime.getSeconds()); + self.set_field(self, "TimeValue", state.state); + } + + } + + function pad(n) + { + return n<10 ? '0'+n : n; + } +} + +function baseiframe(widget_id, url, skin, parameters) +{ + self = this + + // Initialization + + self.parameters = parameters; + + var callbacks = [] + + var monitored_entities = [] + + // Call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Set the url + + if ("url_list" in parameters || "img_list" in parameters || "entity_picture" in parameters) + { + self.index = 0; + refresh_frame(self) + } + + function refresh_frame(self) + { + if ("url_list" in self.parameters) + { + self.set_field(self, "frame_src", self.parameters.url_list[self.index]); + self.set_field(self, "img_src", "/images/Blank.gif"); + size = self.parameters.url_list.length + } + else if ("img_list" in self.parameters) + { + var url = self.parameters.img_list[self.index]; + if (url.indexOf('?') > -1) + { + url = url + "&time=" + Math.floor((new Date).getTime()/1000); + } + else + { + url = url + "?time=" + Math.floor((new Date).getTime()/1000); + } + self.set_field(self, "img_src", url); + size = self.parameters.img_list.length + } + else if ("entity_picture" in self.parameters) + { + var url = self.parameters.entity_picture; + if (url.indexOf('?') > -1) + { + url = url + "&time=" + Math.floor((new Date).getTime()/1000); + } + else + { + url = url + "?time=" + Math.floor((new Date).getTime()/1000); + } + self.set_field(self, "img_src", url); + size = 1 + } + + if ("refresh" in self.parameters) + { + self.index = self.index + 1; + if (self.index == size) + { + self.index = 0; + } + setTimeout(function() {refresh_frame(self)}, self.parameters.refresh * 1000); + } + } +} + +function baseselect(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters; + + self.initial = 1 + + self.onChange = onChange; + + var callbacks = [ + {"observable": "selectedoption", "action": "change", "callback": self.onChange} + ]; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + + if ("entity" in parameters) + { + var monitored_entities = + [ + {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate} + ] + } + else + { + var monitored_entities = [] + } + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + self.state = state; + self.options = state.attributes.options; + set_options(self, self.options, state); + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + if (self.options != state.attributes.options) + { + self.options = state.attributes.options; + set_options(self, self.options, state); + } + if (self.state != state.state) + { + self.state = state.state; + set_value(self, state); + } + } + + function set_value(self, state) + { + value = self.map_state(self, state.state); + self.set_field(self, "selectedoption", value) + } + + function onChange(self, state) + { + if (self.state != self.ViewModel.selectedoption()) + { + self.state = self.ViewModel.selectedoption(); + if (self.initial != 1) + { + args = self.parameters.post_service; + args["option"] = self.state; + self.call_service(self, args) + } + else + { + self.initial = 0 + } + } + } + + function set_options(self, options, state) + { + self.set_field(self, "inputoptions", options) + } + +} + +function basedisplay(widget_id, url, skin, parameters) +{ + // Will be using "self" throughout for the various flavors of "this" + // so for consistency ... + + self = this; + + // Initialization + + self.widget_id = widget_id; + + // Store on brightness or fallback to a default + + // Parameters may come in useful later on + + self.parameters = parameters; + + var callbacks = []; + + // Define callbacks for entities - this model allows a widget to monitor multiple entities if needed + // Initial will be called when the dashboard loads and state has been gathered for the entity + // Update will be called every time an update occurs for that entity + + self.OnStateAvailable = OnStateAvailable; + self.OnStateUpdate = OnStateUpdate; + self.OnSubStateAvailable = OnSubStateAvailable; + self.OnSubStateUpdate = OnSubStateUpdate; + + var monitored_entities = []; + + if ("entity" in parameters && parameters.entity != "") + { + // Make sure that we monitor the entity, not an attribute of it + split_entity = parameters.entity.split(".") + self.entity = split_entity[0] + "." + split_entity[1] + if (split_entity.length > 2) + { + self.entity_attribute = split_entity[2] + } + // Check if the sub_entity should be created by monitoring an attribute of the entity + if ("entity_to_sub_entity_attribute" in parameters && parameters.entity_to_sub_entity_attribute != "") + { + self.sub_entity = self.entity + self.sub_entity_attribute = parameters.entity_to_sub_entity_attribute + } + } + + // Only set up the sub_entity if it was not created already with the entity + attribute + if ("sub_entity" in parameters && parameters.sub_entity != "" && !("sub_entity" in self)) + { + // Make sure that we monitor the sub_entity, not an attribute of it + split_sub_entity = parameters.sub_entity.split(".") + self.sub_entity = split_sub_entity[0] + "." + split_sub_entity[1] + if (split_sub_entity.length > 2) + { + self.sub_entity_attribute = split_sub_entity[2] + } + // Check if the entity should be created by monitoring an attribute of the sub_entity + if ("sub_entity_to_entity_attribute" in parameters && !("entity" in self)) + { + self.entity = self.sub_entity + self.entity_attribute = parameters.sub_entity_to_entity_attribute + } + } + + if ("entity" in self) + { + monitored_entities.push({"entity": self.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}) + } + if ("sub_entity" in self) + { + monitored_entities.push({"entity": self.sub_entity, "initial": self.OnSubStateAvailable, "update": self.OnSubStateUpdate}) + } + + // Finally, call the parent constructor to get things moving + + WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks); + + // Function Definitions + + // The StateAvailable function will be called when + // self.state[] has valid information for the requested entity + // state is the initial state + // Methods + + function OnStateAvailable(self, state) + { + set_value(self, state) + } + + function OnStateUpdate(self, state) + { + set_value(self, state) + } + + function OnSubStateAvailable(self, state) + { + set_sub_value(self, state) + } + + function OnSubStateUpdate(self, state) + { + set_sub_value(self, state) + } + + function set_value(self, state) + { + if ("entity_attribute" in self) { + value = state.attributes[self.entity_attribute] + } + else + { + value = state.state + } + + if (isNaN(value)) + { + self.set_field(self, "value_style", self.parameters.css.text_style); + self.set_field(self, "value", self.map_state(self, value)) + } + else + { + self.set_field(self, "value_style", self.parameters.css.value_style); + self.set_field(self, "value", self.format_number(self, value)); + self.set_field(self, "unit_style", self.parameters.css.unit_style); + if ("units" in self.parameters) + { + self.set_field(self, "unit", self.parameters.units) + } + else + { + self.set_field(self, "unit", state.attributes["unit_of_measurement"]) + } + } + } + + function set_sub_value(self, state) + { + if ("sub_entity_attribute" in self && self.sub_entity_attribute != "") + { + value = state.attributes[self.sub_entity_attribute] + } + else + { + value = state.state + } + + if ("sub_entity_map" in self.parameters) + { + self.set_field(self, "state_text", self.parameters.sub_entity_map[value]) + } + else + { + self.set_field(self, "state_text", value) + } + } +} + diff --git a/ha/appdaemon_config/compiled/javascript/default/hello_init.js b/ha/appdaemon_config/compiled/javascript/default/hello_init.js new file mode 100644 index 0000000..8e27d7e --- /dev/null +++ b/ha/appdaemon_config/compiled/javascript/default/hello_init.js @@ -0,0 +1,130 @@ +var myDeviceID; +$(function(){ //DOM Ready + + function navigate(url) + { + window.location.href = url; + } + + $(document).attr("title", "Hello Panel"); + content_width = (120 + 5) * 8 + 5 + $('.gridster').width(content_width) + $(".gridster ul").gridster({ + widget_margins: [5, 5], + widget_base_dimensions: [120, 120], + avoid_overlapped_widgets: true, + max_rows: 15, + max_size_x: 8, + shift_widgets_up: false + }).data('gridster').disable(); + + // Add Widgets + + var gridster = $(".gridster ul").gridster().data('gridster'); + + gridster.add_widget('
  • ', 2, 2, 1, 1) + + + + var widgets = {} + // Initialize Widgets + + widgets["default-label"] = new basedisplay("default-label", "", "default", {'widget_type': 'basedisplay', 'fields': {'title': '', 'title2': '', 'value': 'Hello World', 'unit': '', 'state_text': ''}, 'static_css': {'title_style': 'color: #fff;', 'title2_style': 'color: #fff;', 'unit_style': '', 'value_style': 'color: #fff;', 'state_text_style': 'color: #fff;', 'widget_style': 'background-color: #444;', 'container_style': ''}, 'css': {}, 'icons': [], 'static_icons': [], 'namespace': 'default'}) + + + // Setup click handler to cancel timeout navigations + + $( ".gridster" ).click(function(){ + clearTimeout(myTimeout); + if (myTimeoutSticky) { + myTimeout = setTimeout(function() { navigate(myTimeoutUrl); }, myTimeoutDelay); + } + }); + + // Set up timeout + + var myTimeout; + var myTimeoutUrl; + var myTimeoutDelay; + var myTimeoutSticky = 0; + if (location.search != "") + { + console.log("begin") + var query = location.search.substr(1); + var result = {}; + query.split("&").forEach(function(part) { + var item = part.split("="); + result[item[0]] = decodeURIComponent(item[1]); + }); + + if ("deviceid" in result) + { + myDeviceID = result.deviceid; + try + { + setCookie('ADdevID', myDeviceID); + } + catch (e) + { + console.log(e); + } + } + else + { + try + { + myDeviceID = getCookie('ADdevID'); + } + catch (e) + { + console.log(e); + myDeviceID = null; + } + } + if ("timeout" in result && "return" in result) + { + url = result.return + argcount = 0 + for (arg in result) + { + if (arg != "timeout" && arg != "return" && arg != "sticky") + { + if (argcount == 0) + { + url += "?"; + } + else + { + url += "&"; + } + argcount ++; + url += arg + "=" + result[arg] + } + } + if ("sticky" in result) + { + myTimeoutSticky = (result.sticky == "1"); + } + myTimeoutUrl = url; + myTimeoutDelay = result.timeout * 1000; + myTimeout = setTimeout(function() { navigate(url); }, result.timeout * 1000); + } + } + else + { + try + { + myDeviceID = getCookie('ADdevID'); + } + catch (e) + { + console.log(e); + myDeviceID = null; + } + } + + // Start listening for AD Events + + window.dashstream = new DashStream("ws", location.protocol, document.domain, location.port, "Hello Panel", widgets); + +}); \ No newline at end of file diff --git a/ha/appdaemon_config/dashboards/Hello.dash b/ha/appdaemon_config/dashboards/Hello.dash new file mode 100755 index 0000000..7ba5112 --- /dev/null +++ b/ha/appdaemon_config/dashboards/Hello.dash @@ -0,0 +1,14 @@ +# +# Main arguments, all optional +# +title: Hello Panel +widget_dimensions: [120, 120] +widget_margins: [5, 5] +columns: 8 + +label: + widget_type: label + text: Hello World + +layout: + - label(2x2) diff --git a/ha/config/automations.yaml b/ha/config/automations.yaml new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/ha/config/automations.yaml @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/ha/config/blueprints/automation/homeassistant/motion_light.yaml b/ha/config/blueprints/automation/homeassistant/motion_light.yaml new file mode 100644 index 0000000..5b389a3 --- /dev/null +++ b/ha/config/blueprints/automation/homeassistant/motion_light.yaml @@ -0,0 +1,55 @@ +blueprint: + name: Motion-activated Light + description: Turn on a light when motion is detected. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml + author: Home Assistant + input: + motion_entity: + name: Motion Sensor + selector: + entity: + domain: binary_sensor + device_class: motion + light_target: + name: Light + selector: + target: + entity: + domain: light + no_motion_wait: + name: Wait time + description: Time to leave the light on after last motion is detected. + default: 120 + selector: + number: + min: 0 + max: 3600 + unit_of_measurement: seconds + +# If motion is detected within the delay, +# we restart the script. +mode: restart +max_exceeded: silent + +trigger: + platform: state + entity_id: !input motion_entity + from: "off" + to: "on" + +action: + - alias: "Turn on the light" + service: light.turn_on + target: !input light_target + - alias: "Wait until there is no motion from device" + wait_for_trigger: + platform: state + entity_id: !input motion_entity + from: "on" + to: "off" + - alias: "Wait the number of seconds that has been set" + delay: !input no_motion_wait + - alias: "Turn off the light" + service: light.turn_off + target: !input light_target diff --git a/ha/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml b/ha/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml new file mode 100644 index 0000000..0798a05 --- /dev/null +++ b/ha/config/blueprints/automation/homeassistant/notify_leaving_zone.yaml @@ -0,0 +1,47 @@ +blueprint: + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml + author: Home Assistant + input: + person_entity: + name: Person + selector: + entity: + domain: person + zone_entity: + name: Zone + selector: + entity: + domain: zone + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + +trigger: + platform: state + entity_id: !input person_entity + +variables: + zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. + zone_state: "{{ states[zone_entity].name }}" + person_entity: !input person_entity + person_name: "{{ states[person_entity].name }}" + +condition: + condition: template + # The first case handles leaving the Home zone which has a special state when zoning called 'home'. + # The second case handles leaving all other zones. + value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + +action: + - alias: "Notify that a person has left the zone" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/ha/config/blueprints/script/homeassistant/confirmable_notification.yaml b/ha/config/blueprints/script/homeassistant/confirmable_notification.yaml new file mode 100644 index 0000000..37e0435 --- /dev/null +++ b/ha/config/blueprints/script/homeassistant/confirmable_notification.yaml @@ -0,0 +1,85 @@ +blueprint: + name: Confirmable Notification + description: >- + A script that sends an actionable notification with a confirmation before + running the specified action. + domain: script + source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml + author: Home Assistant + input: + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + title: + name: "Title" + description: "The title of the button shown in the notification." + default: "" + selector: + text: + message: + name: "Message" + description: "The message body" + selector: + text: + confirm_text: + name: "Confirmation Text" + description: "Text to show on the confirmation button" + default: "Confirm" + selector: + text: + confirm_action: + name: "Confirmation Action" + description: "Action to run when notification is confirmed" + default: [] + selector: + action: + dismiss_text: + name: "Dismiss Text" + description: "Text to show on the dismiss button" + default: "Dismiss" + selector: + text: + dismiss_action: + name: "Dismiss Action" + description: "Action to run when notification is dismissed" + default: [] + selector: + action: + +mode: restart + +sequence: + - alias: "Set up variables" + variables: + action_confirm: "{{ 'CONFIRM_' ~ context.id }}" + action_dismiss: "{{ 'DISMISS_' ~ context.id }}" + - alias: "Send notification" + domain: mobile_app + type: notify + device_id: !input notify_device + title: !input title + message: !input message + data: + actions: + - action: "{{ action_confirm }}" + title: !input confirm_text + - action: "{{ action_dismiss }}" + title: !input dismiss_text + - alias: "Awaiting response" + wait_for_trigger: + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_confirm }}" + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_dismiss }}" + - choose: + - conditions: "{{ wait.trigger.event.data.action == action_confirm }}" + sequence: !input confirm_action + - conditions: "{{ wait.trigger.event.data.action == action_dismiss }}" + sequence: !input dismiss_action diff --git a/ha/config/configuration.yaml b/ha/config/configuration.yaml new file mode 100644 index 0000000..3312548 --- /dev/null +++ b/ha/config/configuration.yaml @@ -0,0 +1,35 @@ + +# Loads default set of integrations. Do not remove. +default_config: + +# Load frontend themes from the themes folder +frontend: + themes: !include_dir_merge_named themes + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +homeassistant: + external_url: https://ha.milvert.com + auth_providers: + - type: homeassistant + - type: legacy_api_password + api_password: !secret http_password + +http: + use_x_forwarded_for: true + ip_ban_enabled: false + trusted_proxies: + - 172.19.0.26 + - 10.0.0.223 + - 172.19.0.39 + +panel_iframe: + configurator: + title: Configurator + icon: mdi:wrench + url: http://10.0.0.3:3218 + require_admin: true + + diff --git a/ha/config/custom_components/huawei_solar b/ha/config/custom_components/huawei_solar new file mode 160000 index 0000000..1e383bb --- /dev/null +++ b/ha/config/custom_components/huawei_solar @@ -0,0 +1 @@ +Subproject commit 1e383bbca8eac8d3d6d50e291ba4130a3eee4ee9 diff --git a/ha/config/scenes.yaml b/ha/config/scenes.yaml new file mode 100644 index 0000000..e69de29 diff --git a/ha/config/scripts.yaml b/ha/config/scripts.yaml new file mode 100644 index 0000000..e69de29 diff --git a/ha/config/secrets.yaml b/ha/config/secrets.yaml new file mode 100644 index 0000000..8a0ccb4 --- /dev/null +++ b/ha/config/secrets.yaml @@ -0,0 +1,5 @@ + +# Use this file to store secrets like usernames and passwords. +# Learn more at https://www.home-assistant.io/docs/configuration/secrets/ +some_password: welcome +http_password: mGwmLFbniCZz7t8JUhGT \ No newline at end of file diff --git a/ha/hass_config/settings.conf b/ha/hass_config/settings.conf new file mode 100644 index 0000000..0a6593f --- /dev/null +++ b/ha/hass_config/settings.conf @@ -0,0 +1,5 @@ +{ + "BASEPATH": "/hass-config", + "ENFORCE_BASEPATH": true, + "DIRSFIRST": true +} diff --git a/librespeed/config/nginx/nginx.conf.sample b/librespeed/config/nginx/nginx.conf.sample index 561260b..40bdfd3 100644 --- a/librespeed/config/nginx/nginx.conf.sample +++ b/librespeed/config/nginx/nginx.conf.sample @@ -1,4 +1,4 @@ -## Version 2022/08/16 - Changelog: https://github.com/linuxserver/docker-baseimage-alpine-nginx/commits/master/root/defaults/nginx/nginx.conf.sample +## Version 2023/04/13 - Changelog: https://github.com/linuxserver/docker-baseimage-alpine-nginx/commits/master/root/defaults/nginx/nginx.conf.sample ### Based on alpine defaults # https://git.alpinelinux.org/aports/tree/main/nginx/nginx.conf?h=3.15-stable @@ -55,7 +55,7 @@ http { tcp_nopush on; # all ssl related config moved to ssl.conf - include /config/nginx/ssl.conf; + # included in server blocks where listen 443 is defined # Enable gzipping of responses. #gzip on; diff --git a/librespeed/config/nginx/site-confs/default.conf.sample b/librespeed/config/nginx/site-confs/default.conf.sample index d580ed7..5dd79c0 100644 --- a/librespeed/config/nginx/site-confs/default.conf.sample +++ b/librespeed/config/nginx/site-confs/default.conf.sample @@ -1,4 +1,4 @@ -## Version 2022/10/04 - Changelog: https://github.com/linuxserver/docker-baseimage-alpine-nginx/commits/master/root/defaults/nginx/site-confs/default.conf.sample +## Version 2023/04/13 - Changelog: https://github.com/linuxserver/docker-baseimage-alpine-nginx/commits/master/root/defaults/nginx/site-confs/default.conf.sample server { listen 80 default_server; @@ -9,6 +9,8 @@ server { server_name _; + include /config/nginx/ssl.conf; + set $root /app/www/public; if (!-d /app/www/public) { set $root /config/www; diff --git a/librespeed/config/nginx/ssl.conf.sample b/librespeed/config/nginx/ssl.conf.sample index 2cc2368..fe83da8 100644 --- a/librespeed/config/nginx/ssl.conf.sample +++ b/librespeed/config/nginx/ssl.conf.sample @@ -1,8 +1,8 @@ -## Version 2022/08/20 - Changelog: https://github.com/linuxserver/docker-baseimage-alpine-nginx/commits/master/root/defaults/nginx/ssl.conf.sample +## Version 2023/06/24 - Changelog: https://github.com/linuxserver/docker-baseimage-alpine-nginx/commits/master/root/defaults/nginx/ssl.conf.sample ### Mozilla Recommendations -# generated 2022-08-05, Mozilla Guideline v5.6, nginx 1.17.7, OpenSSL 1.1.1k, intermediate configuration -# https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1k&guideline=5.6 +# generated 2023-06-25, Mozilla Guideline v5.7, nginx 1.24.0, OpenSSL 3.1.1, intermediate configuration +# https://ssl-config.mozilla.org/#server=nginx&version=1.24.0&config=intermediate&openssl=3.1.1&guideline=5.7 ssl_certificate /config/keys/cert.crt; ssl_certificate_key /config/keys/cert.key; @@ -15,7 +15,7 @@ ssl_dhparam /config/nginx/dhparams.pem; # intermediate configuration ssl_protocols TLSv1.2 TLSv1.3; -ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; # HSTS (ngx_http_headers_module is required) (63072000 seconds) @@ -30,8 +30,8 @@ ssl_prefer_server_ciphers off; # Optional additional headers #add_header Cache-Control "no-transform" always; -#add_header Content-Security-Policy "upgrade-insecure-requests; frame-ancestors 'self'"; -#add_header Permissions-Policy "interest-cohort=()"; +#add_header Content-Security-Policy "upgrade-insecure-requests; frame-ancestors 'self'" always; +#add_header Permissions-Policy "interest-cohort=()" always; #add_header Referrer-Policy "same-origin" always; #add_header X-Content-Type-Options "nosniff" always; #add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/librespeed/config/speedtest_telemetry.sql b/librespeed/config/speedtest_telemetry.sql index 05861e1..bfcae9e 100644 Binary files a/librespeed/config/speedtest_telemetry.sql and b/librespeed/config/speedtest_telemetry.sql differ diff --git a/static_config.yml b/static_config.yml index 502ce0b..0c4f56d 100644 --- a/static_config.yml +++ b/static_config.yml @@ -49,7 +49,7 @@ http: landet_zigbee-service: loadBalancer: servers: - - url: "http://10.1.0.4:8082" + - url: "http://10.0.10.2:8082" lampa-service: loadBalancer: servers: diff --git a/traefik/static_config.yml b/traefik/static_config.yml index 9a03fe4..eaa4078 100644 --- a/traefik/static_config.yml +++ b/traefik/static_config.yml @@ -1,10 +1,10 @@ http: routers: - landet_zigbee-router: - rule: "Host(`zig3.milvert.com`)" - service: landet_zigbee-service + landet-router: + rule: "Host(`landet.milvert.com`)" + service: landet-service middlewares: - - auth + - chain-no-auth entryPoints: - web-secure tls: @@ -46,10 +46,10 @@ http: priority: 1 services: - landet_zigbee-service: + landet-service: loadBalancer: servers: - - url: "http://10.1.0.4:8082" + - url: "http://10.0.10.3:8123" lampa-service: loadBalancer: servers: diff --git a/zigbee2mqtt/configuration.yaml b/zigbee2mqtt/configuration.yaml index c2e3f90..401bebb 100644 --- a/zigbee2mqtt/configuration.yaml +++ b/zigbee2mqtt/configuration.yaml @@ -1,4 +1,4 @@ -homeassistant: false +homeassistant: true permit_join: false mqtt: base_topic: zigbee2mqtt @@ -73,10 +73,6 @@ devices: friendly_name: '0x0017880106fc9e16' '0x00158d0004889e4a': friendly_name: sensor/inne/matrum/th003 - '0x00158d0004889e47': - friendly_name: sensor/ute/garage/th004 - '0x680ae2fffeb15687': - friendly_name: Garage '0x588e81fffe14100f': friendly_name: Ute_soptunna '0x0017880108c48479': @@ -93,6 +89,10 @@ devices: friendly_name: sensor/inne/ada/th002 '0x90fd9ffffe6e8fe2': friendly_name: arum gullampa + '0x847127fffebe9e0d': + friendly_name: '0x847127fffebe9e0d' + '0x5c0272fffe8ab0ce': + friendly_name: Garage_inne_tak groups: '1': friendly_name: Kök_ct diff --git a/zigbee_home_2/configuration_home_2.yaml b/zigbee_home_2/configuration_home_2.yaml index 8060e90..ec45838 100644 --- a/zigbee_home_2/configuration_home_2.yaml +++ b/zigbee_home_2/configuration_home_2.yaml @@ -1,4 +1,4 @@ -homeassistant: false +homeassistant: true permit_join: false mqtt: base_topic: zigbee_home_2 @@ -133,7 +133,23 @@ devices: '0x040d84fffe8b00b0': friendly_name: matrum_fönster_2 '0x847127fffebe9e0d': - friendly_name: '0x847127fffebe9e0d' + friendly_name: sensor/ute/uppfart/m003 + '0x00124b002a51bd18': + friendly_name: sensor/inne/badrum_nere/th013 + '0x00124b002a51c5f1': + friendly_name: sensor/inne/uppe_vrum/th014 + '0xa4c138a7ea206c89': + friendly_name: Garage_spot + '0x00158d00076ab301': + friendly_name: sensor/inne/ada/dt004 + '0x00158d00076ab041': + friendly_name: sensor/inne/oscar/dt005 + '0x00158d00075bdae2': + friendly_name: sensor/inne/uppe_vrum/dt006 + '0x9035eafffefd9103': + friendly_name: Ute_woox + '0x00158d0004889e47': + friendly_name: sensor/ute/garage/th004 groups: '0': friendly_name: default_0