Update HA, add vacuum, grid cards, fixes
3
.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
/*
|
||||
|
||||
# Allow
|
||||
!/.vscode/
|
||||
|
||||
!*.yaml
|
||||
!*.jpg
|
||||
!*.png
|
||||
@ -23,6 +25,7 @@ android/
|
||||
ssh-key/
|
||||
|
||||
ip_bans.yaml
|
||||
google_assistant_service_keys.json
|
||||
secrets.yaml
|
||||
secrets.js
|
||||
known_devices.yaml
|
||||
|
12
.vscode/home-assistant.code-workspace
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
||||
}
|
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
18
README.md
@ -56,4 +56,22 @@ I use the following software, running in docker containers, on my Raspberry Pi:
|
||||
### Security
|
||||
* [Yi Home Camera 1080p](https://amzn.to/2SYhoW6) - with [custom firmware and mqtt add-on](https://github.com/fbrinker/yi-hack-mqtt)
|
||||
|
||||
# Custom stuff
|
||||
|
||||
## Custom Components
|
||||
* https://github.com/And3rsL/Deebot-for-Home-Assistant
|
||||
* https://github.com/MTrab/landroid_cloud
|
||||
* https://github.com/fwestenberg/reolink_dev
|
||||
* https://github.com/thomasloven/hass-fontawesome
|
||||
|
||||
## Lovelace Ressources
|
||||
* https://github.com/bramkragten/weather-card/
|
||||
* https://github.com/denysdovhan/vacuum-card
|
||||
* https://github.com/thomasloven/lovelace-auto-entities
|
||||
* https://github.com/bradcrc/Now-Playing-Card
|
||||
* https://github.com/bradcrc/color-lite-card
|
||||
|
||||
## Packages
|
||||
* https://github.com/Barma-lej/halandroid
|
||||
|
||||
Work in progress. Not all of them are integrated yet.
|
@ -30,13 +30,13 @@ garage_door_open:
|
||||
name: Garagentor geöffnet
|
||||
message: "Zur Info - Die *Garage* steht noch *offen*!"
|
||||
done_message: "Zur Info - Die *Garage* ist wieder *geschlossen*."
|
||||
entity_id: binary_sensor.lumi_garage_door
|
||||
entity_id: binary_sensor.garage_door
|
||||
state: "on"
|
||||
repeat:
|
||||
- 10
|
||||
- 30
|
||||
can_acknowledge: true
|
||||
skip_first: true
|
||||
skip_first: false
|
||||
notifiers:
|
||||
- telegram_group
|
||||
- alexa_all
|
@ -1,6 +1,7 @@
|
||||
# Display Categories: https://developer.amazon.com/de/docs/device-apis/alexa-discovery.html#display-categories
|
||||
smart_home:
|
||||
endpoint: https://api.amazonalexa.com/v3/events
|
||||
endpoint: https://api.eu.amazonalexa.com/v3/events
|
||||
locale: de-DE
|
||||
client_id: !secret alexa_client_id
|
||||
client_secret: !secret alexa_client_secret
|
||||
filter:
|
||||
@ -13,6 +14,7 @@
|
||||
- light.onair
|
||||
- light.stimmungslicht
|
||||
- light.lichterkette
|
||||
- light.philips_iris
|
||||
- switch.livingroom_music
|
||||
- switch.livingroom_netflix
|
||||
- switch.harmony_firetv
|
||||
@ -20,6 +22,7 @@
|
||||
- switch.harmony_playstation
|
||||
- switch.harmony_denon_power
|
||||
- switch.desktop_wol
|
||||
- switch.desktop_jenny_wol
|
||||
- switch.wallboard_display
|
||||
- switch.tplink1
|
||||
- switch.osram_plug_01_57b6060a_on_off
|
||||
@ -43,7 +46,7 @@
|
||||
name: Esstisch
|
||||
description: Esstisch-Lichter
|
||||
light.office_rgb:
|
||||
name: Büro
|
||||
name: Bürolicht
|
||||
description: Büro - Deckenlampe
|
||||
light.lichtleiste:
|
||||
name: Schreibtischlicht
|
||||
@ -57,6 +60,9 @@
|
||||
light.onair:
|
||||
name: Studio-Treppe
|
||||
description: Studio-Treppenlicht
|
||||
light.philips_iris:
|
||||
name: Iris
|
||||
description: Studio-Stimmungslicht
|
||||
switch.livingroom_music:
|
||||
name: Musik
|
||||
description: Wohnzimmer - Musik
|
||||
@ -78,6 +84,9 @@
|
||||
switch.desktop_wol:
|
||||
name: Computer
|
||||
description: Computer im Büro
|
||||
switch.desktop_jenny_wol:
|
||||
name: Ronny
|
||||
description: Jennys Computer
|
||||
switch.wallboard_display:
|
||||
name: Display
|
||||
description: Wallboard Display
|
||||
|
@ -1,3 +1,69 @@
|
||||
- alias: Christmas-Tree on
|
||||
trigger:
|
||||
- platform: time
|
||||
at:
|
||||
- "05:30:00"
|
||||
- "16:00:00"
|
||||
- platform: zone
|
||||
entity_id:
|
||||
- person.jenny
|
||||
zone: zone.home
|
||||
event: enter
|
||||
action:
|
||||
service: light.turn_on
|
||||
data:
|
||||
entity_id:
|
||||
- light.weihnachtsbaum
|
||||
|
||||
- alias: Christmas-Tree off morning
|
||||
trigger:
|
||||
- platform: time
|
||||
at:
|
||||
- "08:00:00"
|
||||
condition:
|
||||
condition: not
|
||||
conditions:
|
||||
- condition: zone
|
||||
entity_id: person.jenny
|
||||
zone: zone.home
|
||||
action:
|
||||
service: light.turn_off
|
||||
data:
|
||||
entity_id:
|
||||
- light.weihnachtsbaum
|
||||
|
||||
- alias: Christmas-Tree off evening
|
||||
trigger:
|
||||
- platform: time
|
||||
at:
|
||||
- "23:00:00"
|
||||
action:
|
||||
service: light.turn_off
|
||||
data:
|
||||
entity_id:
|
||||
- light.weihnachtsbaum
|
||||
|
||||
- alias: Christmas-Tree off Presence
|
||||
trigger:
|
||||
- platform: zone
|
||||
entity_id:
|
||||
- person.jenny
|
||||
- person.florian
|
||||
zone: zone.home
|
||||
event: leave
|
||||
condition:
|
||||
- condition: zone
|
||||
entity_id:
|
||||
- person.jenny
|
||||
- person.florian
|
||||
zone: zone.not_home
|
||||
action:
|
||||
service: light.turn_off
|
||||
data:
|
||||
entity_id:
|
||||
- light.weihnachtsbaum
|
||||
|
||||
|
||||
- alias: Ambilight HDMI
|
||||
trigger:
|
||||
platform: state
|
||||
|
@ -1,25 +1,25 @@
|
||||
- alias: Wallboard On
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.anyone_home
|
||||
to: 'on'
|
||||
- platform: state
|
||||
entity_id: sensor.harmony_activity
|
||||
from: 'Fire TV sehen'
|
||||
action:
|
||||
service: switch.turn_on
|
||||
data:
|
||||
entity_id: switch.wallboard_display
|
||||
#- alias: Wallboard On
|
||||
# trigger:
|
||||
# - platform: state
|
||||
# entity_id: binary_sensor.anyone_home
|
||||
# to: 'on'
|
||||
# - platform: state
|
||||
# entity_id: sensor.harmony_activity
|
||||
# from: 'Fire TV sehen'
|
||||
# action:
|
||||
# service: switch.turn_on
|
||||
# data:
|
||||
# entity_id: switch.wallboard_display
|
||||
|
||||
- alias: Wallboard Off
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.anyone_home
|
||||
to: 'off'
|
||||
- platform: state
|
||||
entity_id: sensor.harmony_activity
|
||||
to: 'Fire TV sehen'
|
||||
action:
|
||||
service: switch.turn_off
|
||||
data:
|
||||
entity_id: switch.wallboard_display
|
||||
#- alias: Wallboard Off
|
||||
# trigger:
|
||||
# - platform: state
|
||||
# entity_id: binary_sensor.anyone_home
|
||||
# to: 'off'
|
||||
# - platform: state
|
||||
# entity_id: sensor.harmony_activity
|
||||
# to: 'Fire TV sehen'
|
||||
# action:
|
||||
# service: switch.turn_off
|
||||
# data:
|
||||
# entity_id: switch.wallboard_display
|
@ -1,9 +1,12 @@
|
||||
- platform: reolink_dev
|
||||
host: !secret cam_livingroom_ip
|
||||
username: !secret cam_livingroom_user
|
||||
password: !secret cam_livingroom_password
|
||||
name: livingroom
|
||||
stream: main
|
||||
protocol: rtmp
|
||||
channel: 0
|
||||
scan_interval: 30
|
||||
- platform: generic
|
||||
name: deebot_dobby_live_map
|
||||
still_image_url: "https://hass.f-brinker.de/local/vacuums/Dobby_liveMap.png"
|
||||
#- platform: reolink_dev
|
||||
# host: !secret cam_livingroom_ip
|
||||
# username: !secret cam_livingroom_user
|
||||
# password: !secret cam_livingroom_password
|
||||
# name: livingroom
|
||||
# stream: main
|
||||
# protocol: rtmp
|
||||
# channel: 0
|
||||
# scan_interval: 30
|
@ -1,4 +1,11 @@
|
||||
person.jenny:
|
||||
entity_picture: "/local/avatars/jenny-bty.jpg"
|
||||
person.florian:
|
||||
entity_picture: "/local/avatars/flo-mtb.jpg"
|
||||
entity_picture: "/local/avatars/flo-mtb.jpg"
|
||||
|
||||
device_tracker.sm_g985f:
|
||||
entity_picture: "/local/avatars/jenny-bty.jpg"
|
||||
icon: mdi:cellphone
|
||||
device_tracker.pixely:
|
||||
entity_picture: "/local/avatars/flo-mtb.jpg"
|
||||
icon: mdi:cellphone
|
63
config/google_assistant.yaml
Normal file
@ -0,0 +1,63 @@
|
||||
# https://console.actions.google.com/u/0/project/home-assistant-e12c3/overview
|
||||
project_id: !secret google_project_id
|
||||
service_account: !include google_assistant_service_keys.json
|
||||
report_state: true
|
||||
expose_by_default: false
|
||||
exposed_domains:
|
||||
- switch
|
||||
- light
|
||||
entity_config:
|
||||
light.ambilight:
|
||||
name: Ambilight
|
||||
room: Wohnzimmer
|
||||
light.kuchen_theke:
|
||||
name: Küche
|
||||
room: Küche
|
||||
light.esstisch:
|
||||
name: Esstisch
|
||||
room: Wohnzimmer
|
||||
light.office_rgb:
|
||||
name: Büro
|
||||
room: Büro
|
||||
light.lichtleiste:
|
||||
name: Schreibtischlicht
|
||||
room: Büro
|
||||
light.stimmungslicht:
|
||||
name: Stimmungslicht
|
||||
room: Wohnzimmer
|
||||
light.lichterkette:
|
||||
name: Lichterkette
|
||||
room: Garten
|
||||
light.weihnachtsbaum:
|
||||
name: Weihnachtsbaum
|
||||
room: Wohnzimmer
|
||||
light.onair:
|
||||
name: Studio-Treppe
|
||||
room: Studio
|
||||
switch.livingroom_music:
|
||||
name: Musik
|
||||
room: Wohnzimmer
|
||||
switch.harmony_firetv:
|
||||
name: Fernseher
|
||||
room: Wohnzimmer
|
||||
switch.harmony_steamlink:
|
||||
name: Konsole
|
||||
room: Wohnzimmer
|
||||
switch.harmony_playstation:
|
||||
name: Playstation
|
||||
room: Wohnzimmer
|
||||
switch.harmony_denon_power:
|
||||
name: Receiver
|
||||
room: Wohnzimmer
|
||||
switch.desktop_wol:
|
||||
name: Computer
|
||||
room: Büro
|
||||
switch.wallboard_display:
|
||||
name: Display
|
||||
room: Wohnzimmer
|
||||
switch.osram_plug_01_57b6060a_on_off:
|
||||
name: Ring
|
||||
room: Studio
|
||||
switch.onair_lamp_recording:
|
||||
name: Aufnahme
|
||||
room: Studio
|
@ -1,7 +1,16 @@
|
||||
- platform: hyperion
|
||||
name: Ambilight
|
||||
host: !secret ambilight_ip
|
||||
hdmi_priority: 900
|
||||
- platform: group
|
||||
name: Küchen-Theke
|
||||
entities:
|
||||
- light.kitchen1
|
||||
- light.kitchen2
|
||||
- platform: group
|
||||
name: Esstisch
|
||||
entities:
|
||||
- light.dining1
|
||||
- light.dining2
|
||||
# - platform: hyperion
|
||||
# name: Ambilight
|
||||
# host: !secret ambilight_ip
|
||||
- platform: group
|
||||
name: onAir
|
||||
entities:
|
||||
@ -9,7 +18,7 @@
|
||||
- light.innr_gu10_rgb_2
|
||||
- platform: switch
|
||||
name: Lichterkette
|
||||
entity_id: switch.innr_steckdose
|
||||
entity_id: switch.garden_chain_of_lights
|
||||
- platform: switch
|
||||
name: Stimmungslicht
|
||||
entity_id: switch.livingroom_stimmungslicht
|
||||
entity_id: switch.moodlight
|
54
config/scripts/robots.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
vacuum_living_room:
|
||||
alias: "Wohnzimmer saugen"
|
||||
sequence:
|
||||
- service: vacuum.send_command
|
||||
data:
|
||||
entity_id: vacuum.dobby
|
||||
command: spot_area
|
||||
params:
|
||||
rooms: 0
|
||||
cleanings: 1
|
||||
|
||||
vacuum_kitchen:
|
||||
alias: "Küche saugen"
|
||||
sequence:
|
||||
- service: vacuum.send_command
|
||||
data:
|
||||
entity_id: vacuum.dobby
|
||||
command: spot_area
|
||||
params:
|
||||
rooms: 1
|
||||
cleanings: 1
|
||||
|
||||
vacuum_corridor:
|
||||
alias: "Flur saugen"
|
||||
sequence:
|
||||
- service: vacuum.send_command
|
||||
data:
|
||||
entity_id: vacuum.dobby
|
||||
command: spot_area
|
||||
params:
|
||||
rooms: 2
|
||||
cleanings: 1
|
||||
|
||||
vacuum_laundry:
|
||||
alias: "HWR saugen"
|
||||
sequence:
|
||||
- service: vacuum.send_command
|
||||
data:
|
||||
entity_id: vacuum.dobby
|
||||
command: spot_area
|
||||
params:
|
||||
rooms: 3
|
||||
cleanings: 1
|
||||
|
||||
vacuum_dining_room:
|
||||
alias: "Esszimmer saugen"
|
||||
sequence:
|
||||
- service: vacuum.send_command
|
||||
data:
|
||||
entity_id: vacuum.dobby
|
||||
command: spot_area
|
||||
params:
|
||||
rooms: 4
|
||||
cleanings: 1
|
@ -1,5 +0,0 @@
|
||||
- platform: template
|
||||
sensors:
|
||||
device_mobile_fb_battery:
|
||||
value_template: '{{ states.device_tracker.mobile_fb.attributes.battery }}'
|
||||
unit_of_measurement: '%'
|
@ -1,9 +0,0 @@
|
||||
- platform: template
|
||||
sensors:
|
||||
motion_livingroom:
|
||||
friendly_name: Motion Livingroom
|
||||
device_class: motion
|
||||
entity_id: camera.livingroom
|
||||
value_template: "{{ is_state('camera.livingroom', 'motion') }}"
|
||||
delay_off:
|
||||
seconds: 30
|
@ -14,6 +14,12 @@
|
||||
count: 2
|
||||
scan_interval: 15
|
||||
|
||||
- platform: ping
|
||||
name: desktop_jenny_ping
|
||||
host: !secret desktop_jenny_ip
|
||||
count: 2
|
||||
scan_interval: 15
|
||||
|
||||
- platform: template
|
||||
sensors:
|
||||
anyone_home:
|
||||
|
@ -1,6 +1,6 @@
|
||||
- platform: command_line
|
||||
name: Wallboard HDMI Status
|
||||
command: !secret wallboard_hdmi_status_cmd
|
||||
payload_on: display_power=1
|
||||
payload_off: display_power=0
|
||||
scan_interval: 60
|
||||
# - platform: command_line
|
||||
# name: Wallboard HDMI Status
|
||||
# command: !secret wallboard_hdmi_status_cmd
|
||||
# payload_on: display_power=1
|
||||
# payload_off: display_power=0
|
||||
# scan_interval: 60
|
@ -1,53 +0,0 @@
|
||||
- platform: template
|
||||
switches:
|
||||
|
||||
camera_livingroom_email:
|
||||
value_template: "{{ is_state_attr('camera.livingroom', 'email_enabled', true) }}"
|
||||
turn_on:
|
||||
service: camera.enable_email
|
||||
data:
|
||||
entity_id: camera.livingroom
|
||||
turn_off:
|
||||
service: camera.disable_email
|
||||
data:
|
||||
entity_id: camera.livingroom
|
||||
icon_template: >-
|
||||
{% if is_state_attr('camera.livingroom', 'email_enabled', true) %}
|
||||
mdi:email
|
||||
{% else %}
|
||||
mdi:email-outline
|
||||
{% endif %}
|
||||
|
||||
camera_livingroom_ftp:
|
||||
value_template: "{{ is_state_attr('camera.livingroom', 'ftp_enabled', true) }}"
|
||||
turn_on:
|
||||
service: camera.enable_ftp
|
||||
data:
|
||||
entity_id: camera.livingroom
|
||||
turn_off:
|
||||
service: camera.disable_ftp
|
||||
data:
|
||||
entity_id: camera.livingroom
|
||||
icon_template: >-
|
||||
{% if is_state_attr('camera.livingroom', 'ftp_enabled', true) %}
|
||||
mdi:filmstrip
|
||||
{% else %}
|
||||
mdi:filmstrip-off
|
||||
{% endif %}
|
||||
|
||||
camera_livingroom_ir_lights:
|
||||
value_template: "{{ is_state_attr('camera.livingroom', 'ir_lights_enabled', true) }}"
|
||||
turn_on:
|
||||
service: camera.enable_ir_lights
|
||||
data:
|
||||
entity_id: camera.livingroom
|
||||
turn_off:
|
||||
service: camera.disable_ir_lights
|
||||
data:
|
||||
entity_id: camera.livingroom
|
||||
icon_template: >-
|
||||
{% if is_state_attr('camera.livingroom', 'ir_lights_enabled', true) %}
|
||||
mdi:flashlight
|
||||
{% else %}
|
||||
mdi:flashlight-off
|
||||
{% endif %}
|
@ -14,6 +14,19 @@
|
||||
turn_off:
|
||||
service: script.dummy
|
||||
|
||||
desktop_jenny_wol:
|
||||
value_template: "{{ is_state('binary_sensor.desktop_jenny_ping', 'on') }}"
|
||||
turn_on:
|
||||
service: shell_command.ssh
|
||||
data:
|
||||
sshkey: !secret sshkey_wakeonlan
|
||||
host: !secret nas_ip
|
||||
user: !secret nas_ssh_user
|
||||
port: !secret nas_ssh_port
|
||||
command_or_param: !secret desktop_jenny_mac
|
||||
turn_off:
|
||||
service: script.dummy
|
||||
|
||||
onair_lamp_recording:
|
||||
value_template: "{{ is_state('input_boolean.onair_lamp_recording', 'on') }}"
|
||||
turn_on:
|
||||
|
@ -1,10 +1,9 @@
|
||||
- platform: template
|
||||
switches:
|
||||
|
||||
wallboard_display:
|
||||
friendly_name: Wallboard Display Toggle
|
||||
value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}"
|
||||
turn_on:
|
||||
service: script.wallboard_hdmi_on
|
||||
turn_off:
|
||||
service: script.wallboard_hdmi_off
|
||||
#- platform: template
|
||||
# switches:
|
||||
# wallboard_display:
|
||||
# friendly_name: Wallboard Display Toggle
|
||||
# value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}"
|
||||
# turn_on:
|
||||
# service: script.wallboard_hdmi_on
|
||||
# turn_off:
|
||||
# service: script.wallboard_hdmi_off
|
@ -1,20 +1,15 @@
|
||||
icon: mdi:cellphone-link
|
||||
path: devices
|
||||
cards:
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: sensor.myip
|
||||
name: Öffentliche IP
|
||||
icon: mdi:earth
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: entities
|
||||
title: Florian
|
||||
show_header_toggle: false
|
||||
entities:
|
||||
- entity: device_tracker.pixel_4
|
||||
- entity: device_tracker.pixely
|
||||
name: Standort
|
||||
- entity: sensor.battery_level
|
||||
- entity: sensor.pixely_akkufullstand
|
||||
name: Handy-Akku
|
||||
icon: mdi:battery
|
||||
- type: entities
|
||||
@ -29,28 +24,23 @@ cards:
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: entities
|
||||
title: Wake On Lan
|
||||
title: WOL Florian
|
||||
show_header_toggle: false
|
||||
entities:
|
||||
- entity: switch.desktop_wol
|
||||
name: Desktop-PC
|
||||
icon: mdi:desktop-classic
|
||||
- type: entities
|
||||
title: Wallboard
|
||||
title: WOL Jenny
|
||||
show_header_toggle: false
|
||||
entities:
|
||||
- entity: switch.wallboard_display
|
||||
name: Display
|
||||
icon: mdi:tablet
|
||||
- entity: switch.desktop_jenny_wol
|
||||
name: Desktop-PC Ronny
|
||||
icon: mdi:desktop-classic
|
||||
- type: entities
|
||||
title: Home Assistant Slaves
|
||||
title: Wallboard
|
||||
show_header_toggle: false
|
||||
entities:
|
||||
- type: weblink
|
||||
name: Livingroom
|
||||
url: !secret url_haslave_livingroom
|
||||
icon: mdi:home-assistant
|
||||
- type: weblink
|
||||
name: Office
|
||||
url: !secret url_haslave_office
|
||||
icon: mdi:home-assistant
|
||||
- entity: switch.wallboard_display
|
||||
name: Display
|
||||
icon: mdi:tablet
|
@ -1,6 +1,7 @@
|
||||
#icon: mdi:home-variant-outline
|
||||
title: Haus
|
||||
path: floor
|
||||
icon: 'mdi:floor-plan'
|
||||
panel: true
|
||||
cards:
|
||||
- type: picture-elements
|
||||
|
@ -1,151 +1,103 @@
|
||||
icon: mdi:water-percent
|
||||
path: humidity
|
||||
cards:
|
||||
- type: vertical-stack
|
||||
|
||||
- type: grid
|
||||
title: Wohnzimmer
|
||||
columns: 2
|
||||
square: true
|
||||
cards:
|
||||
- type: markdown
|
||||
content: "### Elternbad"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.hygro_bathroom_parents_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.hygro_bathroom_parents_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: markdown
|
||||
content: "### \"Kinderbad\""
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.hygro_bathroom_kids_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.hygro_bathroom_kids_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: markdown
|
||||
content: "### Schlafzimmer"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.lumi_bedroom_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.lumi_bedroom_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: markdown
|
||||
content: "### Gästezimmer"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.lumi_guestroom_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.lumi_guestroom_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: vertical-stack
|
||||
- type: sensor
|
||||
entity: sensor.lumi_livingroom_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
- type: sensor
|
||||
entity: sensor.lumi_livingroom_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
|
||||
- type: grid
|
||||
title: Schlafzimmer
|
||||
columns: 2
|
||||
square: true
|
||||
cards:
|
||||
- type: markdown
|
||||
content: "### Wohnzimmer"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.lumi_livingroom_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.lumi_livingroom_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: markdown
|
||||
content: "### Hauswirtschaftsraum"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.hygro_hwr_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.hygro_hwr_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: markdown
|
||||
content: "### Büro"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.lumi_office_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.lumi_office_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: markdown
|
||||
content: "### Dachboden"
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.attic_humidity_2
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.attic_temperature_2
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 12
|
||||
- type: sensor
|
||||
entity: sensor.lumi_bedroom_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
- type: sensor
|
||||
entity: sensor.lumi_bedroom_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
|
||||
- type: grid
|
||||
title: Gästezimmer
|
||||
columns: 2
|
||||
square: true
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.lumi_guestroom_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
- type: sensor
|
||||
entity: sensor.lumi_guestroom_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
|
||||
- type: grid
|
||||
title: Büro
|
||||
columns: 2
|
||||
square: true
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.lumi_office_humidity
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
- type: sensor
|
||||
entity: sensor.lumi_office_temperature
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
|
||||
- type: grid
|
||||
title: Dachboden
|
||||
columns: 2
|
||||
square: true
|
||||
cards:
|
||||
- type: sensor
|
||||
entity: sensor.attic_humidity_2
|
||||
name: Luftfeuchtigkeit
|
||||
graph: line
|
||||
unit: "%"
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
- type: sensor
|
||||
entity: sensor.attic_temperature_2
|
||||
name: Temperatur
|
||||
graph: line
|
||||
unit: °C
|
||||
detail: 2
|
||||
hours_to_show: 24
|
||||
|
@ -1,24 +1,42 @@
|
||||
title: Lichter
|
||||
path: lights
|
||||
icon: 'mdi:lightbulb'
|
||||
cards:
|
||||
- type: light
|
||||
entity: light.kuchen_theke
|
||||
name: Küchen-Theke
|
||||
- type: light
|
||||
entity: light.stimmungslicht
|
||||
name: Stimmungslicht
|
||||
- type: light
|
||||
entity: light.ambilight
|
||||
name: Ambilight
|
||||
- type: light
|
||||
entity: light.lichterkette
|
||||
name: Lichterkette (Garten)
|
||||
- type: light
|
||||
entity: light.esstisch
|
||||
name: Esstisch
|
||||
- type: light
|
||||
entity: light.tint_rgb_gu10_1
|
||||
name: Kinderbad
|
||||
- type: grid
|
||||
title: Erdgeschoss
|
||||
columns: 3
|
||||
suqare: true
|
||||
cards:
|
||||
- type: light
|
||||
entity: light.kuchen_theke
|
||||
name: Küchen-Theke
|
||||
- type: light
|
||||
entity: light.esstisch
|
||||
name: Esstisch
|
||||
- type: light
|
||||
entity: light.stimmungslicht
|
||||
name: Stimmungslicht
|
||||
- type: light
|
||||
entity: light.ambilight
|
||||
name: Ambilight
|
||||
- type: light
|
||||
entity: light.lichterkette
|
||||
name: Garten
|
||||
|
||||
- type: grid
|
||||
title: 1. Stock
|
||||
columns: 3
|
||||
suqare: true
|
||||
cards:
|
||||
- type: light
|
||||
entity: light.office_rgb
|
||||
name: Bürolicht
|
||||
- type: light
|
||||
entity: light.lichtleiste
|
||||
name: Schreibtischlicht
|
||||
- type: light
|
||||
entity: light.tint_rgb_gu10_1
|
||||
name: Kinderbad
|
||||
|
||||
- type: vertical-stack
|
||||
title: Studio
|
||||
@ -29,19 +47,11 @@ cards:
|
||||
- type: entities
|
||||
show_header_toggle: false
|
||||
entities:
|
||||
- entity: light.philips_iris
|
||||
name: Iris
|
||||
- entity: switch.onair_lamp_recording
|
||||
name: Studio Aufnahme
|
||||
icon: mdi:camera-rear
|
||||
- entity: switch.osram_plug_01_57b6060a_on_off
|
||||
name: Studio Ringlicht
|
||||
icon: mdi:checkbox-blank-circle-outline
|
||||
|
||||
- type: vertical-stack
|
||||
title: Büro
|
||||
cards:
|
||||
- type: light
|
||||
entity: light.office_rgb
|
||||
name: Deckenlicht
|
||||
- type: light
|
||||
entity: light.lichtleiste
|
||||
name: Schreibtischlicht
|
||||
|
@ -1,20 +1,9 @@
|
||||
title: Wohnzimmer
|
||||
path: livingroom
|
||||
title: Media
|
||||
path: media
|
||||
icon: 'mdi:theater'
|
||||
cards:
|
||||
# Lights
|
||||
- type: horizontal-stack
|
||||
cards:
|
||||
- type: entities
|
||||
entities:
|
||||
- entity: light.kuchen_theke
|
||||
name: Küchen-Theke
|
||||
- entity: light.esstisch
|
||||
name: Esstisch
|
||||
- entity: light.stimmungslicht
|
||||
name: Stimmungslicht
|
||||
icon: mdi:lightbulb
|
||||
- entity: light.ambilight
|
||||
name: Ambilight
|
||||
- type: media-control
|
||||
entity: media_player.spotify
|
||||
|
||||
# Denon + Harmony
|
||||
- type: vertical-stack
|
||||
@ -45,14 +34,11 @@ cards:
|
||||
icon: mdi:bluetooth-audio
|
||||
- entity: switch.harmony_steamlink
|
||||
icon: mdi:steam
|
||||
- entity: switch.harmony_playstation
|
||||
icon: mdi:playstation
|
||||
state_image:
|
||||
"PowerOff": /local/images/power.jpg
|
||||
"Fire TV sehen": /local/images/firetv.jpg
|
||||
"Musik Bluetooth": /local/images/music.jpg
|
||||
"SteamLink": /local/images/steamlink.jpg
|
||||
"PlayStation": /local/images/playstation.jpg
|
||||
entity: sensor.harmony_activity
|
||||
|
||||
# Spotify
|
@ -3,6 +3,10 @@ path: network
|
||||
cards:
|
||||
- type: vertical-stack
|
||||
cards:
|
||||
- type: entity
|
||||
entity: sensor.myip
|
||||
name: Öffentliche IP
|
||||
icon: mdi:earth
|
||||
- type: gauge
|
||||
entity: sensor.adguard_average_processing_speed
|
||||
max: 100
|
||||
|
@ -1,5 +1,6 @@
|
||||
title: Übersicht
|
||||
path: overview
|
||||
icon: "mdi:tablet-dashboard"
|
||||
cards:
|
||||
- type: conditional
|
||||
conditions:
|
||||
@ -16,11 +17,12 @@ cards:
|
||||
- type: vertical-stack
|
||||
cards:
|
||||
- type: custom:weather-card
|
||||
entity: weather.openweathermap
|
||||
name: Wetter
|
||||
- type: iframe
|
||||
url: !secret iframe_windy
|
||||
aspect_ratio: 75%
|
||||
entity: weather.openweathermap
|
||||
icons: "/local/lovelace/custom/weather-card/icons/"
|
||||
# - type: iframe
|
||||
# url: !secret iframe_windy
|
||||
# aspect_ratio: 75%
|
||||
# - type: iframe
|
||||
# url: !secret iframe_earth
|
||||
# aspect_ratio: 75%
|
||||
@ -45,7 +47,7 @@ cards:
|
||||
- person.florian
|
||||
- person.jenny
|
||||
- type: map
|
||||
aspect_ratio: 75%
|
||||
default_zoom: 15
|
||||
entities:
|
||||
- zone.home
|
||||
aspect_ratio: 60%
|
||||
entities:
|
||||
- device_tracker.sm_g985f
|
||||
- device_tracker.pixel_4
|
@ -1,8 +1,48 @@
|
||||
title: Mäheroboter
|
||||
path: mower
|
||||
title: Roboter
|
||||
path: robots
|
||||
badges: []
|
||||
icon: 'mdi:robot-vacuum-variant'
|
||||
cards:
|
||||
# Dobby
|
||||
- type: 'custom:vacuum-card'
|
||||
entity: vacuum.dobby
|
||||
map: camera.deebot_dobby_live_map
|
||||
stats:
|
||||
default:
|
||||
- entity_id: sensor.dobby_heap
|
||||
unit: Stunden
|
||||
subtitle: Filter
|
||||
- entity_id: sensor.dobby_sidebrush
|
||||
unit: Stunden
|
||||
subtitle: Seitenbürste
|
||||
- entity_id: sensor.dobby_brush
|
||||
unit: Stunden
|
||||
subtitle: Hauptbürste
|
||||
cleaning:
|
||||
- entity_id: sensor.dobby_stats_area
|
||||
unit: m2
|
||||
subtitle: Gereinigter Bereich
|
||||
- entity_id: sensor.dobby_stats_time
|
||||
unit: Minuten
|
||||
subtitle: Dauer
|
||||
actions:
|
||||
- name: Wohnzimmer
|
||||
service: script.vacuum_living_room
|
||||
icon: 'mdi:sofa'
|
||||
- name: Küche
|
||||
service: script.vacuum_kitchen
|
||||
icon: 'mdi:pot-steam'
|
||||
- name: Esszimmer
|
||||
service: script.vacuum_dining_room
|
||||
icon: 'mdi:table-furniture'
|
||||
- name: HWR
|
||||
service: script.vacuum_laundry
|
||||
icon: 'mdi:washing-machine'
|
||||
- name: Flur
|
||||
service: script.vacuum_corridor
|
||||
icon: 'mdi:door'
|
||||
|
||||
# Hans-Dieter
|
||||
- type: vertical-stack
|
||||
cards:
|
||||
- elements:
|
@ -1,5 +0,0 @@
|
||||
icon: mdi:spotify
|
||||
path: spotify
|
||||
cards:
|
||||
- type: media-control
|
||||
entity: media_player.spotify
|
@ -1,5 +1,7 @@
|
||||
homeassistant:
|
||||
name: Home
|
||||
internal_url: !secret url_internal
|
||||
external_url: !secret url_external
|
||||
latitude: !secret home_lat
|
||||
longitude: !secret home_long
|
||||
elevation: !secret home_elevation
|
||||
@ -15,12 +17,11 @@ homeassistant:
|
||||
- 192.168.0.0/16
|
||||
- fd00::/8
|
||||
# landroid
|
||||
packages: !include_dir_named packages
|
||||
#packages: !include_dir_named packages
|
||||
|
||||
config:
|
||||
conversation:
|
||||
device_tracker:
|
||||
#discovery:
|
||||
dhcp:
|
||||
history:
|
||||
logbook:
|
||||
map:
|
||||
@ -35,31 +36,36 @@ http:
|
||||
ip_ban_enabled: true
|
||||
login_attempts_threshold: 5
|
||||
use_x_forwarded_for: true
|
||||
base_url: !secret url_base
|
||||
trusted_proxies:
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
# traefik.web
|
||||
- 172.19.0.0/24
|
||||
|
||||
frontend:
|
||||
themes: !include_dir_merge_named themes/
|
||||
|
||||
lovelace:
|
||||
mode: yaml
|
||||
resources:
|
||||
- url: /local/lovelace/custom/weather-card/weather-card.js
|
||||
type: module
|
||||
- url: /local/lovelace/custom/vacuum-card/vacuum-card.js
|
||||
type: module
|
||||
- url: /local/lovelace/custom/auto-entities/auto-entities.js
|
||||
type: module
|
||||
- url: /local/lovelace/custom/color-lite-card/color-lite-card.js
|
||||
type: module
|
||||
- url: /local/lovelace/custom/now-playing-card/now-playing-card.js
|
||||
type: module
|
||||
|
||||
tts:
|
||||
- platform: google_translate
|
||||
service_name: google_say
|
||||
|
||||
zha:
|
||||
usb_path: /dev/ttyACM0
|
||||
radio_type: deconz
|
||||
database_path: /config/zigbee.db
|
||||
|
||||
emulated_roku:
|
||||
servers:
|
||||
- name: Home Assistant
|
||||
listen_port: !secret roku_port
|
||||
|
||||
mqtt:
|
||||
broker: !secret mqtt_broker_ip
|
||||
username: !secret mqtt_username
|
||||
@ -90,32 +96,18 @@ notify:
|
||||
- platform: command_line
|
||||
name: alexa_all
|
||||
command: "/config/alexa_wrapper.sh -d 'ALL'"
|
||||
# - name: android
|
||||
# platform: fcm-android
|
||||
|
||||
media_player:
|
||||
- platform: androidtv
|
||||
device_class: firetv
|
||||
name: Fire TV
|
||||
host: !secret firetv_ip
|
||||
adbkey: !secret adbkey
|
||||
get_sources: true
|
||||
|
||||
remote:
|
||||
- platform: harmony
|
||||
name: Livingroom Harmony
|
||||
host: !secret harmonyhub_ip
|
||||
delay_secs: 0.3
|
||||
|
||||
person:
|
||||
- name: Jenny
|
||||
id: jenny
|
||||
user_id: !secret userId_jenny
|
||||
device_trackers:
|
||||
- device_tracker.sm_g985f
|
||||
- name: Florian
|
||||
id: florian
|
||||
user_id: !secret userId_florian
|
||||
device_trackers:
|
||||
- device_tracker.pixel_4
|
||||
- device_tracker.pixely
|
||||
|
||||
zone:
|
||||
- name: !secret work_name_f
|
||||
@ -129,10 +121,6 @@ zone:
|
||||
radius: 500
|
||||
icon: mdi:briefcase
|
||||
|
||||
weather:
|
||||
- platform: openweathermap
|
||||
api_key: !secret openweathermap
|
||||
|
||||
ffmpeg:
|
||||
ffmpeg_bin: /usr/bin/ffmpeg
|
||||
|
||||
@ -198,20 +186,27 @@ spotify:
|
||||
client_id: !secret spotify_client_id
|
||||
client_secret: !secret spotify_client_secret
|
||||
|
||||
ecovacs:
|
||||
deebot:
|
||||
username: !secret ecovacs_user
|
||||
password: !secret ecovacs_password
|
||||
country: de
|
||||
continent: eu
|
||||
deviceid:
|
||||
- !secret ecovacs_serial_dobby
|
||||
live_map: true
|
||||
show_color_rooms: true
|
||||
livemappath: 'www/vacuums/'
|
||||
|
||||
#tplink:
|
||||
# discovery: false
|
||||
# switch:
|
||||
# - host: !secret tplink_ip
|
||||
fontawesome:
|
||||
regular:
|
||||
solid:
|
||||
brands:
|
||||
|
||||
# External config files
|
||||
alert: !include_dir_merge_named config/alerts/
|
||||
alexa: !include config/alexa.yaml
|
||||
google_assistant: !include config/google_assistant.yaml
|
||||
|
||||
alert: !include_dir_merge_named config/alerts/
|
||||
automation: !include_dir_merge_list config/automations/
|
||||
binary_sensor: !include_dir_merge_list config/sensors_binary/
|
||||
camera: !include config/cameras.yaml
|
||||
|
119
custom_components/deebot/__init__.py
Normal file
@ -0,0 +1,119 @@
|
||||
"""Support for Deebot Vaccums."""
|
||||
import asyncio
|
||||
import logging
|
||||
import async_timeout
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import base64
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from datetime import timedelta
|
||||
from deebotozmo import *
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
REQUIREMENTS = ['deebotozmo==1.7.8']
|
||||
|
||||
CONF_COUNTRY = "country"
|
||||
CONF_CONTINENT = "continent"
|
||||
CONF_DEVICEID = "deviceid"
|
||||
CONF_LIVEMAPPATH = "livemappath"
|
||||
CONF_LIVEMAP = "live_map"
|
||||
CONF_SHOWCOLORROOMS = "show_color_rooms"
|
||||
DEEBOT_DEVICES = "deebot_devices"
|
||||
|
||||
# Generate a random device ID on each bootup
|
||||
DEEBOT_API_DEVICEID = "".join(
|
||||
random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HUB = None
|
||||
DOMAIN = 'deebot'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string),
|
||||
vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string),
|
||||
vol.Required(CONF_DEVICEID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_LIVEMAP, default=True): cv.boolean,
|
||||
vol.Optional(CONF_SHOWCOLORROOMS, default=False): cv.boolean,
|
||||
vol.Optional(CONF_LIVEMAPPATH, default='www/'): cv.string
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Deebot."""
|
||||
global HUB
|
||||
|
||||
HUB = DeebotHub(config[DOMAIN])
|
||||
|
||||
for component in ('sensor', 'binary_sensor', 'vacuum'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
class DeebotHub(Entity):
|
||||
"""Deebot Hub"""
|
||||
|
||||
def __init__(self, domain_config):
|
||||
"""Initialize the Deebot Vacuum."""
|
||||
|
||||
self.config = domain_config
|
||||
self._lock = threading.Lock()
|
||||
|
||||
self.ecovacs_api = EcoVacsAPI(
|
||||
DEEBOT_API_DEVICEID,
|
||||
domain_config.get(CONF_USERNAME),
|
||||
EcoVacsAPI.md5(domain_config.get(CONF_PASSWORD)),
|
||||
domain_config.get(CONF_COUNTRY),
|
||||
domain_config.get(CONF_CONTINENT)
|
||||
)
|
||||
|
||||
devices = self.ecovacs_api.devices()
|
||||
liveMapEnabled = domain_config.get(CONF_LIVEMAP)
|
||||
liveMapRooms = domain_config.get(CONF_SHOWCOLORROOMS)
|
||||
country = domain_config.get(CONF_COUNTRY).lower()
|
||||
continent = domain_config.get(CONF_CONTINENT).lower()
|
||||
self.vacbots = []
|
||||
|
||||
# CREATE VACBOT FOR EACH DEVICE
|
||||
for device in devices:
|
||||
if device['name'] in domain_config.get(CONF_DEVICEID):
|
||||
vacbot = VacBot(
|
||||
self.ecovacs_api.uid,
|
||||
self.ecovacs_api.resource,
|
||||
self.ecovacs_api.user_access_token,
|
||||
device,
|
||||
country,
|
||||
continent,
|
||||
liveMapEnabled,
|
||||
liveMapRooms
|
||||
)
|
||||
|
||||
_LOGGER.debug("New vacbot found: " + device['name'])
|
||||
|
||||
self.vacbots.append(vacbot)
|
||||
|
||||
_LOGGER.debug("Hub initialized")
|
||||
|
||||
@Throttle(timedelta(seconds=10))
|
||||
def update(self):
|
||||
""" Update all statuses. """
|
||||
try:
|
||||
for vacbot in self.vacbots:
|
||||
vacbot.request_all_statuses()
|
||||
except Exception as ex:
|
||||
_LOGGER.error('Update failed: %s', ex)
|
||||
raise
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Return the name of the hub."""
|
||||
return "Deebot Hub"
|
BIN
custom_components/deebot/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
custom_components/deebot/__pycache__/sensor.cpython-38.pyc
Normal file
BIN
custom_components/deebot/__pycache__/vacuum.cpython-38.pyc
Normal file
48
custom_components/deebot/binary_sensor.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""Support for Deebot Sensor."""
|
||||
from typing import Optional
|
||||
|
||||
from deebotozmo import *
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
|
||||
from . import HUB as hub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Deebot binary sensor platform."""
|
||||
hub.update()
|
||||
|
||||
for vacbot in hub.vacbots:
|
||||
add_devices([DeebotMopAttachedBinarySensor(vacbot, "mop_attached")], True)
|
||||
|
||||
|
||||
class DeebotMopAttachedBinarySensor(BinarySensorEntity):
|
||||
"""Deebot mop attached binary sensor"""
|
||||
|
||||
def __init__(self, vacbot: VacBot, device_id: str):
|
||||
"""Initialize the Sensor."""
|
||||
self._vacbot = vacbot
|
||||
self._id = device_id
|
||||
|
||||
if self._vacbot.vacuum.get("nick", None) is not None:
|
||||
self._vacbot_name = "{}".format(self._vacbot.vacuum["nick"])
|
||||
else:
|
||||
# In case there is no nickname defined, use the device id
|
||||
self._vacbot_name = "{}".format(self._vacbot.vacuum["did"])
|
||||
|
||||
self._name = self._vacbot_name + "_" + device_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self._vacbot.mop_attached
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:water" if self.is_on else "mdi:water-off"
|
11
custom_components/deebot/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "deebot",
|
||||
"name": "Deebot for Hassio",
|
||||
"documentation": "https://github.com/And3rsL/Deebot-for-hassio",
|
||||
"requirements": [
|
||||
"deebotozmo==1.7.8"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@And3rsL"],
|
||||
"homeassistant": "0.110.0"
|
||||
}
|
179
custom_components/deebot/sensor.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""Support for Deebot Sensor."""
|
||||
from typing import Optional
|
||||
|
||||
from deebotozmo import *
|
||||
from homeassistant.const import (STATE_UNKNOWN)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import HUB as hub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
STATE_CLEANING,
|
||||
STATE_DOCKED,
|
||||
STATE_ERROR,
|
||||
STATE_IDLE,
|
||||
STATE_PAUSED,
|
||||
STATE_RETURNING,
|
||||
)
|
||||
|
||||
STATE_CODE_TO_STATE = {
|
||||
'STATE_IDLE': STATE_IDLE,
|
||||
'STATE_CLEANING': STATE_CLEANING,
|
||||
'STATE_RETURNING': STATE_RETURNING,
|
||||
'STATE_DOCKED': STATE_DOCKED,
|
||||
'STATE_ERROR': STATE_ERROR,
|
||||
'STATE_PAUSED': STATE_PAUSED,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Deebot sensor."""
|
||||
hub.update()
|
||||
|
||||
for vacbot in hub.vacbots:
|
||||
# General
|
||||
add_devices([DeebotLastCleanImageSensor(vacbot, "last_clean_image")], True)
|
||||
add_devices([DeebotWaterLevelSensor(vacbot, "water_level")], True)
|
||||
|
||||
# Components
|
||||
add_devices([DeebotComponentSensor(vacbot, COMPONENT_MAIN_BRUSH)], True)
|
||||
add_devices([DeebotComponentSensor(vacbot, COMPONENT_SIDE_BRUSH)], True)
|
||||
add_devices([DeebotComponentSensor(vacbot, COMPONENT_FILTER)], True)
|
||||
|
||||
# Stats
|
||||
add_devices([DeebotStatsSensor(vacbot, "stats_area")], True)
|
||||
add_devices([DeebotStatsSensor(vacbot, "stats_time")], True)
|
||||
add_devices([DeebotStatsSensor(vacbot, "stats_type")], True)
|
||||
|
||||
|
||||
class DeebotBaseSensor(Entity):
|
||||
"""Deebot base sensor"""
|
||||
|
||||
def __init__(self, vacbot, device_id):
|
||||
"""Initialize the Sensor."""
|
||||
|
||||
self._state = STATE_UNKNOWN
|
||||
self._vacbot = vacbot
|
||||
self._id = device_id
|
||||
|
||||
if self._vacbot.vacuum.get("nick", None) is not None:
|
||||
self._vacbot_name = "{}".format(self._vacbot.vacuum["nick"])
|
||||
else:
|
||||
# In case there is no nickname defined, use the device id
|
||||
self._vacbot_name = "{}".format(self._vacbot.vacuum["did"])
|
||||
|
||||
self._name = self._vacbot_name + "_" + device_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
|
||||
class DeebotLastCleanImageSensor(DeebotBaseSensor):
|
||||
"""Deebot Sensor"""
|
||||
|
||||
def __init__(self, vacbot, device_id):
|
||||
"""Initialize the Sensor."""
|
||||
super(DeebotLastCleanImageSensor, self).__init__(vacbot, device_id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the vacuum cleaner."""
|
||||
if self._vacbot.last_clean_image is not None:
|
||||
return self._vacbot.last_clean_image
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:image-search"
|
||||
|
||||
|
||||
class DeebotWaterLevelSensor(DeebotBaseSensor):
|
||||
"""Deebot Sensor"""
|
||||
|
||||
def __init__(self, vacbot, device_id):
|
||||
"""Initialize the Sensor."""
|
||||
super(DeebotWaterLevelSensor, self).__init__(vacbot, device_id)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the vacuum cleaner."""
|
||||
|
||||
if self._vacbot.water_level is not None:
|
||||
return self._vacbot.water_level
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:water"
|
||||
|
||||
|
||||
class DeebotComponentSensor(DeebotBaseSensor):
|
||||
"""Deebot Sensor"""
|
||||
|
||||
def __init__(self, vacbot, device_id):
|
||||
"""Initialize the Sensor."""
|
||||
super(DeebotComponentSensor, self).__init__(vacbot, device_id)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return '%'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the vacuum cleaner."""
|
||||
|
||||
for key, val in self._vacbot.components.items():
|
||||
if key == self._id:
|
||||
return int(val)
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
if self._id == COMPONENT_MAIN_BRUSH or self._id == COMPONENT_SIDE_BRUSH:
|
||||
return "mdi:broom"
|
||||
elif self._id == COMPONENT_FILTER:
|
||||
return "mdi:air-filter"
|
||||
|
||||
|
||||
class DeebotStatsSensor(DeebotBaseSensor):
|
||||
"""Deebot Sensor"""
|
||||
|
||||
def __init__(self, vacbot, device_id):
|
||||
"""Initialize the Sensor."""
|
||||
super(DeebotStatsSensor, self).__init__(vacbot, device_id)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self._id == 'stats_area':
|
||||
return "mq"
|
||||
elif self._id == 'stats_time':
|
||||
return "min"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the vacuum cleaner."""
|
||||
|
||||
if self._id == 'stats_area' and self._vacbot.stats_area is not None:
|
||||
return int(self._vacbot.stats_area)
|
||||
elif self._id == 'stats_time' and self._vacbot.stats_time is not None:
|
||||
return int(self._vacbot.stats_time/60)
|
||||
elif self._id == 'stats_type':
|
||||
return self._vacbot.stats_type
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def icon(self) -> Optional[str]:
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
if self._id == 'stats_area':
|
||||
return "mdi:floor-plan"
|
||||
elif self._id == 'stats_time':
|
||||
return "mdi:timer-outline"
|
||||
elif self._id == 'stats_type':
|
||||
return "mdi:cog"
|
247
custom_components/deebot/vacuum.py
Normal file
@ -0,0 +1,247 @@
|
||||
"""Support for Deebot Vaccums."""
|
||||
import base64
|
||||
from typing import Optional, Dict, Any, Union, List
|
||||
|
||||
from deebotozmo import *
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import HUB as hub
|
||||
|
||||
CONF_COUNTRY = "country"
|
||||
CONF_CONTINENT = "continent"
|
||||
CONF_DEVICEID = "deviceid"
|
||||
CONF_LIVEMAPPATH = "livemappath"
|
||||
CONF_LIVEMAP = "live_map"
|
||||
CONF_SHOWCOLORROOMS = "show_color_rooms"
|
||||
DEEBOT_DEVICES = "deebot_devices"
|
||||
|
||||
from homeassistant.components.vacuum import (
|
||||
PLATFORM_SCHEMA,
|
||||
STATE_CLEANING,
|
||||
STATE_DOCKED,
|
||||
STATE_ERROR,
|
||||
STATE_IDLE,
|
||||
STATE_PAUSED,
|
||||
STATE_RETURNING,
|
||||
SUPPORT_BATTERY,
|
||||
SUPPORT_FAN_SPEED,
|
||||
SUPPORT_LOCATE,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_RETURN_HOME,
|
||||
SUPPORT_SEND_COMMAND,
|
||||
SUPPORT_START,
|
||||
SUPPORT_STATE,
|
||||
VacuumEntity,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_DEEBOT = (
|
||||
SUPPORT_BATTERY
|
||||
| SUPPORT_FAN_SPEED
|
||||
| SUPPORT_LOCATE
|
||||
| SUPPORT_PAUSE
|
||||
| SUPPORT_RETURN_HOME
|
||||
| SUPPORT_SEND_COMMAND
|
||||
| SUPPORT_START
|
||||
| SUPPORT_STATE
|
||||
)
|
||||
|
||||
STATE_CODE_TO_STATE = {
|
||||
'STATE_IDLE': STATE_IDLE,
|
||||
'STATE_CLEANING': STATE_CLEANING,
|
||||
'STATE_RETURNING': STATE_RETURNING,
|
||||
'STATE_DOCKED': STATE_DOCKED,
|
||||
'STATE_ERROR': STATE_ERROR,
|
||||
'STATE_PAUSED': STATE_PAUSED,
|
||||
}
|
||||
|
||||
ATTR_COMPONENT_PREFIX = "component_"
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Deebot vacuums."""
|
||||
if DEEBOT_DEVICES not in hass.data:
|
||||
hass.data[DEEBOT_DEVICES] = []
|
||||
|
||||
for vacbot in hub.vacbots:
|
||||
vacuum = DeebotVacuum(hass, vacbot)
|
||||
add_devices([vacuum])
|
||||
|
||||
class DeebotVacuum(VacuumEntity):
|
||||
"""Deebot Vacuums"""
|
||||
|
||||
def __init__(self, hass, vacbot):
|
||||
"""Initialize the Deebot Vacuum."""
|
||||
self._hass = hass
|
||||
|
||||
self.device = vacbot
|
||||
|
||||
if self.device.vacuum.get("nick", None) is not None:
|
||||
self._name = "{}".format(self.device.vacuum["nick"])
|
||||
else:
|
||||
# In case there is no nickname defined, use the device id
|
||||
self._name = "{}".format(self.device.vacuum["did"])
|
||||
|
||||
self._fan_speed = None
|
||||
self._live_map = None
|
||||
self._live_map_path = hub.config.get(CONF_LIVEMAPPATH) + self._name + '_liveMap.png'
|
||||
|
||||
self.device.refresh_statuses()
|
||||
|
||||
_LOGGER.debug("Vacuum initialized: %s", self.name)
|
||||
|
||||
def on_fan_change(self, fan_speed):
|
||||
self._fan_speed = fan_speed
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return an unique ID."""
|
||||
return self.device.vacuum.get("did", None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag vacuum cleaner robot features that are supported."""
|
||||
return SUPPORT_DEEBOT
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the vacuum cleaner."""
|
||||
if self.device.vacuum_status is not None and self.device.is_available == True:
|
||||
return STATE_CODE_TO_STATE[self.device.vacuum_status]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.device.is_available
|
||||
|
||||
async def async_return_to_base(self, **kwargs):
|
||||
"""Set the vacuum cleaner to return to the dock."""
|
||||
await self.hass.async_add_executor_job(self.device.Charge)
|
||||
|
||||
@property
|
||||
def battery_level(self):
|
||||
"""Return the battery level of the vacuum cleaner."""
|
||||
if self.device.battery_status is not None:
|
||||
return self.device.battery_status
|
||||
|
||||
return super().battery_level
|
||||
|
||||
@property
|
||||
def fan_speed(self):
|
||||
"""Return the fan speed of the vacuum cleaner."""
|
||||
return self.device.fan_speed
|
||||
|
||||
async def async_set_fan_speed(self, fan_speed, **kwargs):
|
||||
await self.hass.async_add_executor_job(self.device.SetFanSpeed, fan_speed)
|
||||
|
||||
@property
|
||||
def fan_speed_list(self):
|
||||
"""Get the list of available fan speed steps of the vacuum cleaner."""
|
||||
return [FAN_SPEED_QUIET, FAN_SPEED_NORMAL, FAN_SPEED_MAX, FAN_SPEED_MAXPLUS]
|
||||
|
||||
async def async_pause(self):
|
||||
"""Pause the vacuum cleaner."""
|
||||
await self.hass.async_add_executor_job(self.device.CleanPause)
|
||||
|
||||
async def async_start(self):
|
||||
"""Start the vacuum cleaner."""
|
||||
await self.hass.async_add_executor_job(self.device.CleanResume)
|
||||
|
||||
async def async_locate(self, **kwargs):
|
||||
"""Locate the vacuum cleaner."""
|
||||
await self.hass.async_add_executor_job(self.device.PlaySound)
|
||||
|
||||
async def async_send_command(self, command, params=None, **kwargs):
|
||||
"""Send a command to a vacuum cleaner."""
|
||||
_LOGGER.debug("async_send_command %s (%s), %s", command, params, kwargs)
|
||||
|
||||
if command == 'spot_area':
|
||||
await self.hass.async_add_executor_job(self.device.SpotArea, params['rooms'], params['cleanings'])
|
||||
return
|
||||
|
||||
if command == 'custom_area':
|
||||
await self.hass.async_add_executor_job(self.device.CustomArea, params['coordinates'], params['cleanings'])
|
||||
return
|
||||
|
||||
if command == 'set_water':
|
||||
await self.hass.async_add_executor_job(self.device.SetWaterLevel, params['amount'])
|
||||
return
|
||||
|
||||
if command == 'relocate':
|
||||
await self.hass.async_add_executor_job(self.device.Relocate)
|
||||
return
|
||||
|
||||
if command == 'auto_clean':
|
||||
self.hass.async_add_executor_job(self.device.Clean, params['type'])
|
||||
return
|
||||
|
||||
if command == 'refresh_components':
|
||||
await self.hass.async_add_executor_job(self.device.refresh_components)
|
||||
return
|
||||
|
||||
if command == 'refresh_statuses':
|
||||
await self.hass.async_add_executor_job(self.device.refresh_statuses)
|
||||
return
|
||||
|
||||
if command == 'refresh_live_map':
|
||||
await self.hass.async_add_executor_job(self.device.refresh_liveMap)
|
||||
return
|
||||
|
||||
if command == 'save_live_map':
|
||||
if(self._live_map != self.device.live_map):
|
||||
self._live_map = self.device.live_map
|
||||
with open(params['path'], "wb") as fh:
|
||||
fh.write(base64.decodebytes(self.device.live_map))
|
||||
|
||||
await self.hass.async_add_executor_job(self.device.exc_command, command, params)
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch state from the device."""
|
||||
await self.hass.async_add_executor_job(self.device.request_all_statuses)
|
||||
|
||||
try:
|
||||
if(self._live_map != self.device.live_map):
|
||||
self._live_map = self.device.live_map
|
||||
with open(self._live_map_path, "wb") as fh:
|
||||
fh.write(base64.decodebytes(self.device.live_map))
|
||||
except KeyError:
|
||||
_LOGGER.warning("Can't access local folder: %s", self._live_map_path)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return device specific state attributes.
|
||||
|
||||
Implemented by platform classes. Convention for attribute names
|
||||
is lowercase snake_case.
|
||||
"""
|
||||
|
||||
data: Dict[str, Union[int, List[int]]] = {}
|
||||
|
||||
# Needed for custom vacuum-card (https://github.com/denysdovhan/vacuum-card)
|
||||
# Should find a better way without breaking everyone rooms script
|
||||
data['status'] = STATE_CODE_TO_STATE[self.device.vacuum_status]
|
||||
|
||||
if self.device.getSavedRooms() is not None:
|
||||
for r in self.device.getSavedRooms():
|
||||
# convert room name to snake_case to meet the convention
|
||||
room_name = "room_" + slugify(r["subtype"])
|
||||
room_values = data.get(room_name)
|
||||
if room_values is None:
|
||||
data[room_name] = r["id"]
|
||||
elif isinstance(room_values, list):
|
||||
room_values.append(r["id"])
|
||||
else:
|
||||
# Convert from int to list
|
||||
data[room_name] = [room_values, r["id"]]
|
||||
|
||||
return data
|
50
custom_components/fontawesome/__init__.py
Normal file
@ -0,0 +1,50 @@
|
||||
DOMAIN = "fontawesome"
|
||||
|
||||
DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url'
|
||||
ICONS_URL = f'/{DOMAIN}/'
|
||||
ICON_FILES = {
|
||||
'regular': 'far.js',
|
||||
'solid': 'fas.js',
|
||||
'brands': 'fab.js',
|
||||
}
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
for f in ICON_FILES.values():
|
||||
hass.http.register_static_path(
|
||||
f"/{DOMAIN}/{f}",
|
||||
hass.config.path(f"custom_components/{DOMAIN}/data/{f}"),
|
||||
True
|
||||
)
|
||||
conf = config.get(DOMAIN)
|
||||
if not conf:
|
||||
return True
|
||||
register_modules(hass, conf)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
config_entry.add_update_listener(_update_listener)
|
||||
register_modules(hass, config_entry.options)
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass, config_entry):
|
||||
register_modules(hass, [])
|
||||
return True
|
||||
|
||||
|
||||
async def _update_listener(hass, config_entry):
|
||||
register_modules(hass, config_entry.options)
|
||||
return True
|
||||
|
||||
|
||||
def register_modules(hass, modules):
|
||||
if DATA_EXTRA_MODULE_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_MODULE_URL] = set()
|
||||
url_set = hass.data[DATA_EXTRA_MODULE_URL]
|
||||
|
||||
for k, v in ICON_FILES.items():
|
||||
url_set.discard(ICONS_URL+v)
|
||||
if k in modules and modules[k] is not False:
|
||||
url_set.add(ICONS_URL+v)
|
49
custom_components/fontawesome/config_flow.py
Normal file
@ -0,0 +1,49 @@
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register("fontawesome")
|
||||
class FontawesomeConfigFlow(config_entries.ConfigFlow):
|
||||
async def async_step_user(self, user_input=None):
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
return FontawesomeEditFlow(config_entry)
|
||||
|
||||
|
||||
class FontawesomeEditFlow(config_entries.OptionsFlow):
|
||||
def __init__(self, config_entry):
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
"regular",
|
||||
default=self.config_entry.options.get("regular", False),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
"solid",
|
||||
default=self.config_entry.options.get("solid", False),
|
||||
): bool,
|
||||
vol.Optional(
|
||||
"brands",
|
||||
default=self.config_entry.options.get("brands", False),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
)
|
1
custom_components/fontawesome/data/fab.js
Normal file
1
custom_components/fontawesome/data/far.js
Normal file
1
custom_components/fontawesome/data/fas.js
Normal file
9
custom_components/fontawesome/manifest.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"domain": "fontawesome",
|
||||
"name": "Fontawesome icons",
|
||||
"documentation": "",
|
||||
"dependencies": ["frontend"],
|
||||
"codeowners": [],
|
||||
"requirements": [],
|
||||
"config_flow": true
|
||||
}
|
21
custom_components/fontawesome/translations/en.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "FontAwesome",
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only a single configuration of FontAwesome is allowed."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Icon sets",
|
||||
"description": "Which icon sets to include",
|
||||
"data": {
|
||||
"regular": "Include Regular icons (far:)",
|
||||
"solid": "Include Solid icons (fas:)",
|
||||
"brands": "Include Brand icons (fab:)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -57,11 +57,11 @@ API_WORX_SENSORS = {
|
||||
},
|
||||
"icon": "mdi:battery",
|
||||
"unit": "%",
|
||||
"device_class": None,
|
||||
"device_class": "battery",
|
||||
},
|
||||
"error": {
|
||||
"state": {"error_description": "state", "error": "error_id"},
|
||||
"icon": None,
|
||||
"icon": "mdi:alert",
|
||||
"unit": None,
|
||||
"device_class": None,
|
||||
},
|
||||
@ -83,6 +83,8 @@ API_WORX_SENSORS = {
|
||||
"rain_delay": "raindelay",
|
||||
"schedule_variation": "timeextension",
|
||||
"firmware": "firmware_version",
|
||||
"serial": "serial",
|
||||
"mac": "mac",
|
||||
},
|
||||
"icon": None,
|
||||
"unit": None,
|
||||
@ -130,7 +132,7 @@ async def async_setup(hass, config):
|
||||
async def handle_start(call):
|
||||
"""Handle start service call."""
|
||||
if "id" in call.data:
|
||||
ID = call.data["id"]
|
||||
ID = int(call.data["id"])
|
||||
|
||||
for cli in client:
|
||||
attrs = vars(cli)
|
||||
@ -144,7 +146,7 @@ async def async_setup(hass, config):
|
||||
async def handle_pause(call):
|
||||
"""Handle pause service call."""
|
||||
if "id" in call.data:
|
||||
ID = call.data["id"]
|
||||
ID = int(call.data["id"])
|
||||
|
||||
for cli in client:
|
||||
attrs = vars(cli)
|
||||
@ -158,7 +160,7 @@ async def async_setup(hass, config):
|
||||
async def handle_home(call):
|
||||
"""Handle pause service call."""
|
||||
if "id" in call.data:
|
||||
ID = call.data["id"]
|
||||
ID = int(call.data["id"])
|
||||
|
||||
for cli in client:
|
||||
attrs = vars(cli)
|
||||
@ -177,31 +179,46 @@ async def async_setup(hass, config):
|
||||
|
||||
if "id" in call.data:
|
||||
_LOGGER.debug("Data from Home Assistant: %s", call.data["id"])
|
||||
|
||||
|
||||
for cli in client:
|
||||
attrs = vars(cli)
|
||||
if (attrs["id"] == call.data["id"]):
|
||||
if (attrs["id"] == int(call.data["id"])):
|
||||
break
|
||||
else:
|
||||
id += 1
|
||||
|
||||
if "raindelay" in call.data:
|
||||
tmpdata["rd"] = call.data["raindelay"]
|
||||
tmpdata["rd"] = int(call.data["raindelay"])
|
||||
_LOGGER.debug("Setting rain_delay for %s to %s", client[id].name, call.data["raindelay"])
|
||||
sendData = True
|
||||
|
||||
if "timeextension" in call.data:
|
||||
tmpdata["sc"] = {}
|
||||
tmpdata["sc"]["p"] = call.data["timeextension"]
|
||||
tmpdata["sc"] = {}
|
||||
tmpdata["sc"]["p"] = int(call.data["timeextension"])
|
||||
data = json.dumps(tmpdata)
|
||||
_LOGGER.debug("Setting time_extension for %s to %s", client[id].name, call.data["timeextension"])
|
||||
sendData = True
|
||||
|
||||
if "multizone_distances" in call.data:
|
||||
tmpdata["mz"] = [int(x) for x in call.data["multizone_distances"]]
|
||||
data = json.dumps(tmpdata)
|
||||
_LOGGER.debug("Setting multizone distances for %s to %s", client[id].name, call.data["multizone_distances"])
|
||||
sendData = True
|
||||
|
||||
if "multizone_probabilities" in call.data:
|
||||
tmpdata["mzv"] = []
|
||||
for idx, val in enumerate(call.data["multizone_probabilities"]):
|
||||
for _ in range(val):
|
||||
tmpdata["mzv"].append(idx)
|
||||
data = json.dumps(tmpdata)
|
||||
_LOGGER.debug("Setting multizone probabilities for %s to %s", client[id].name, call.data["multizone_probabilities"])
|
||||
sendData = True
|
||||
|
||||
if sendData:
|
||||
data = json.dumps(tmpdata)
|
||||
_LOGGER.debug("Sending: %s", data)
|
||||
client[id].sendData(data)
|
||||
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_CONFIG, handle_config)
|
||||
|
||||
return True
|
||||
|
@ -1,7 +1,9 @@
|
||||
{
|
||||
"domain": "landroid_cloud",
|
||||
"name": "Worx Landroid Cloud",
|
||||
"documentation": "https://www.home-assistant.io/integrations/landroid_cloud/",
|
||||
"requirements": ["pyworxcloud==1.2.17"],
|
||||
"documentation": "https://github.com/MTrab/landroid_cloud/blob/master/README.md",
|
||||
"issue_tracker": "https://github.com/MTrab/landroid_cloud/issues",
|
||||
"requirements": ["pyworxcloud==1.2.21"],
|
||||
"version": "1.6.5",
|
||||
"codeowners": ["@MTrab"]
|
||||
}
|
||||
|
@ -1,127 +1,132 @@
|
||||
"""Support for monitoring Worx Landroid Sensors."""
|
||||
import async_timeout
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components import sensor
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_INITIALIZING = "Initializing"
|
||||
STATE_OFFLINE = "Offline"
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the available sensors for Worx Landroid."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
entities = []
|
||||
|
||||
info = discovery_info[0]
|
||||
for tSensor in API_WORX_SENSORS:
|
||||
name = "{}_{}".format(info["name"].lower(), tSensor.lower())
|
||||
friendly_name = "{} {}".format(info["friendly"], tSensor)
|
||||
dev_id = info["id"]
|
||||
api = hass.data[LANDROID_API][dev_id]
|
||||
sensor_type = tSensor
|
||||
_LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"])
|
||||
entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id)
|
||||
entities.append(entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class LandroidSensor(Entity):
|
||||
"""Class to create and populate a Landroid Sensor."""
|
||||
|
||||
def __init__(self, api, name, sensor_type, friendly_name, dev_id):
|
||||
"""Init new sensor."""
|
||||
|
||||
self._api = api
|
||||
self._attributes = {}
|
||||
self._available = False
|
||||
self._name = friendly_name
|
||||
self._state = STATE_INITIALIZING
|
||||
self._sensor_type = sensor_type
|
||||
self._dev_id = dev_id
|
||||
self.entity_id = sensor.ENTITY_ID_FORMAT.format(name)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return sensor attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return API_WORX_SENSORS[self._sensor_type]["unit"]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend."""
|
||||
return API_WORX_SENSORS[self._sensor_type]["icon"]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False as entity is updated from the component."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return sensor state."""
|
||||
return self._state
|
||||
|
||||
@callback
|
||||
def update_callback(self):
|
||||
"""Get new data and update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Connect update callbacks."""
|
||||
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback)
|
||||
|
||||
def _get_data(self):
|
||||
"""Return new data from the api cache."""
|
||||
data = self._api.get_data(self._sensor_type)
|
||||
self._available = True
|
||||
return data
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
_LOGGER.debug("Updating %s", self.entity_id)
|
||||
data = self._get_data()
|
||||
if "state" in data:
|
||||
_LOGGER.debug(data)
|
||||
state = data.pop("state")
|
||||
_LOGGER.debug("Mower %s State %s", self._name, state)
|
||||
self._attributes.update(data)
|
||||
self._state = state
|
||||
else:
|
||||
_LOGGER.debug("No data received for %s", self.entity_id)
|
||||
reachable = self._api._client.online
|
||||
if not reachable:
|
||||
if "_battery" in self.entity_id:
|
||||
self._state = "Unknown"
|
||||
else:
|
||||
self._state = STATE_OFFLINE
|
||||
#else:
|
||||
# attrs = vars(self._api._client)
|
||||
# for item in attrs:
|
||||
# _LOGGER.debug("%s : %s", item, attrs[item])
|
||||
"""Support for monitoring Worx Landroid Sensors."""
|
||||
import async_timeout
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components import sensor
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_INITIALIZING = "Initializing"
|
||||
STATE_OFFLINE = "Offline"
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the available sensors for Worx Landroid."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
entities = []
|
||||
|
||||
info = discovery_info[0]
|
||||
for tSensor in API_WORX_SENSORS:
|
||||
name = "{}_{}".format(info["name"].lower(), tSensor.lower())
|
||||
friendly_name = "{} {}".format(info["friendly"], tSensor)
|
||||
dev_id = info["id"]
|
||||
api = hass.data[LANDROID_API][dev_id]
|
||||
sensor_type = tSensor
|
||||
_LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"])
|
||||
entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id)
|
||||
entities.append(entity)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class LandroidSensor(Entity):
|
||||
"""Class to create and populate a Landroid Sensor."""
|
||||
|
||||
def __init__(self, api, name, sensor_type, friendly_name, dev_id):
|
||||
"""Init new sensor."""
|
||||
|
||||
self._api = api
|
||||
self._attributes = {}
|
||||
self._available = False
|
||||
self._name = friendly_name
|
||||
self._state = STATE_INITIALIZING
|
||||
self._sensor_type = sensor_type
|
||||
self._dev_id = dev_id
|
||||
self.entity_id = sensor.ENTITY_ID_FORMAT.format(name)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return sensor attributes."""
|
||||
return self._attributes
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return API_WORX_SENSORS[self._sensor_type]["unit"]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend."""
|
||||
if self._sensor_type == "battery" and isinstance(self.state, int):
|
||||
charging = self._attributes["charging"]
|
||||
return icon_for_battery_level(battery_level=self.state, charging=charging)
|
||||
|
||||
return API_WORX_SENSORS[self._sensor_type]["icon"]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False as entity is updated from the component."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return sensor state."""
|
||||
return self._state
|
||||
|
||||
@callback
|
||||
def update_callback(self):
|
||||
"""Get new data and update state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Connect update callbacks."""
|
||||
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback)
|
||||
|
||||
def _get_data(self):
|
||||
"""Return new data from the api cache."""
|
||||
data = self._api.get_data(self._sensor_type)
|
||||
self._available = True
|
||||
return data
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the sensor."""
|
||||
_LOGGER.debug("Updating %s", self.entity_id)
|
||||
data = self._get_data()
|
||||
if "state" in data:
|
||||
_LOGGER.debug(data)
|
||||
state = data.pop("state")
|
||||
_LOGGER.debug("Mower %s State %s", self._name, state)
|
||||
self._attributes.update(data)
|
||||
self._state = state
|
||||
if "latitude" in self._attributes:
|
||||
if self._attributes["latitude"] == None:
|
||||
del self._attributes["latitude"]
|
||||
del self._attributes["longitude"]
|
||||
else:
|
||||
_LOGGER.debug("No data received for %s", self.entity_id)
|
||||
reachable = self._api._client.online
|
||||
if not reachable:
|
||||
if "_battery" in self.entity_id:
|
||||
self._state = STATE_UNKNOWN
|
||||
else:
|
||||
self._state = STATE_OFFLINE
|
||||
|
@ -28,3 +28,9 @@ config:
|
||||
timeextension:
|
||||
description: Set time extension. Extension in % ranging from -100 to 100
|
||||
example: -23
|
||||
multizone_distances:
|
||||
description: Set multizone distances. Distances in meter. 0 = Disabled
|
||||
example: '[15, 80, 120, 155]'
|
||||
multizone_probabilities:
|
||||
description: Set multizone probabilities. Probabilities in parts-of-ten. 1 = 10%, 2 = 20%, ...
|
||||
example: '[5, 1, 2, 2]'
|
||||
|
@ -1 +0,0 @@
|
||||
"""Reolink Camera component for HomeAssistant."""
|
@ -1,278 +0,0 @@
|
||||
"""
|
||||
Reolink Camera API
|
||||
"""
|
||||
import requests
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class ReolinkApi(object):
|
||||
def __init__(self, ip, channel):
|
||||
self._url = "http://" + ip + "/cgi-bin/api.cgi"
|
||||
self._ip = ip
|
||||
self._channel = channel
|
||||
self._token = None
|
||||
self._motion_state = False
|
||||
self._last_motion = 0
|
||||
self._device_info = None
|
||||
self._motion_state = None
|
||||
self._ftp_state = None
|
||||
self._email_state = None
|
||||
self._ir_state = None
|
||||
self._rtspport = None
|
||||
self._rtmpport = None
|
||||
self._ptzpresets = dict()
|
||||
|
||||
def status(self):
|
||||
if self._token is None:
|
||||
return
|
||||
|
||||
param_channel = {"channel": self._channel}
|
||||
body = [{"cmd": "GetDevInfo", "action":1, "param": param_channel},
|
||||
{"cmd": "GetNetPort", "action": 1, "param": param_channel},
|
||||
{"cmd": "GetFtp", "action": 1, "param": param_channel},
|
||||
{"cmd": "GetEmail", "action": 1, "param": param_channel},
|
||||
{"cmd": "GetIrLights", "action": 1, "param": param_channel},
|
||||
{"cmd": "GetPtzPreset", "action": 1, "param": param_channel}]
|
||||
|
||||
param = {"token": self._token}
|
||||
response = self.send(body, param)
|
||||
|
||||
try:
|
||||
json_data = json.loads(response.text)
|
||||
except:
|
||||
_LOGGER.error(f"Error translating response to json")
|
||||
return
|
||||
|
||||
for data in json_data:
|
||||
try:
|
||||
if data["cmd"] == "GetDevInfo":
|
||||
self._device_info = data
|
||||
|
||||
elif data["cmd"] == "GetNetPort":
|
||||
self._netport_settings = data
|
||||
self._rtspport = data["value"]["NetPort"]["rtspPort"]
|
||||
self._rtmpport = data["value"]["NetPort"]["rtmpPort"]
|
||||
|
||||
elif data["cmd"] == "GetFtp":
|
||||
self._ftp_settings = data
|
||||
if (data["value"]["Ftp"]["schedule"]["enable"] == 1):
|
||||
self._ftp_state = True
|
||||
else:
|
||||
self._ftp_state = False
|
||||
|
||||
elif data["cmd"] == "GetEmail":
|
||||
self._email_settings = data
|
||||
if (data["value"]["Email"]["schedule"]["enable"] == 1):
|
||||
self._email_state = True
|
||||
else:
|
||||
self._email_state = False
|
||||
|
||||
elif data["cmd"] == "GetIrLights":
|
||||
self._ir_settings = data
|
||||
if (data["value"]["IrLights"]["state"] == "Auto"):
|
||||
self._ir_state = True
|
||||
else:
|
||||
self._ir_state = False
|
||||
|
||||
elif data["cmd"] == "GetPtzPreset":
|
||||
self._ptzpresets_settings = data
|
||||
for preset in data["value"]["PtzPreset"]:
|
||||
if int(preset["enable"]) == 1:
|
||||
preset_name = preset["name"]
|
||||
preset_id = int(preset["id"])
|
||||
self._ptzpresets[preset_name] = preset_id
|
||||
_LOGGER.debug(f"Got preset {preset_name} with ID {preset_id}")
|
||||
else:
|
||||
_LOGGER.debug(f"Preset is not enabled: {preset}")
|
||||
except:
|
||||
continue
|
||||
|
||||
@property
|
||||
def motion_state(self):
|
||||
body = [{"cmd": "GetMdState", "action": 0, "param":{"channel":self._channel}}]
|
||||
param = {"token": self._token}
|
||||
|
||||
response = self.send(body, param)
|
||||
|
||||
try:
|
||||
json_data = json.loads(response.text)
|
||||
|
||||
if json_data is None:
|
||||
_LOGGER.error(f"Unable to get Motion detection state at IP {self._ip}")
|
||||
self._motion_state = False
|
||||
return self._motion_state
|
||||
|
||||
if json_data[0]["value"]["state"] == 1:
|
||||
self._motion_state = True
|
||||
self._last_motion = datetime.datetime.now()
|
||||
else:
|
||||
self._motion_state = False
|
||||
except:
|
||||
self._motion_state = False
|
||||
|
||||
return self._motion_state
|
||||
|
||||
@property
|
||||
def still_image(self):
|
||||
response = self.send(None, f"?cmd=Snap&channel={self._channel}&token={self._token}", stream=True)
|
||||
response.raw.decode_content = True
|
||||
return response.raw
|
||||
|
||||
@property
|
||||
def snapshot(self):
|
||||
response = self.send(None, f"?cmd=Snap&channel={self._channel}&token={self._token}", stream=False)
|
||||
return response.content
|
||||
|
||||
@property
|
||||
def ftp_state(self):
|
||||
return self._ftp_state
|
||||
|
||||
@property
|
||||
def email_state(self):
|
||||
return self._email_state
|
||||
|
||||
@property
|
||||
def ir_state(self):
|
||||
return self._ir_state
|
||||
|
||||
@property
|
||||
def rtmpport(self):
|
||||
return self._rtmpport
|
||||
|
||||
@property
|
||||
def rtspport(self):
|
||||
return self._rtspport
|
||||
|
||||
@property
|
||||
def last_motion(self):
|
||||
return self._last_motion
|
||||
|
||||
@property
|
||||
def ptzpresets(self):
|
||||
return self._ptzpresets
|
||||
|
||||
def login(self, username, password):
|
||||
body = [{"cmd": "Login", "action": 0, "param": {"User": {"userName": username, "password": password}}}]
|
||||
param = {"cmd": "Login", "token": "null"}
|
||||
|
||||
response = self.send(body, param)
|
||||
|
||||
try:
|
||||
json_data = json.loads(response.text)
|
||||
except:
|
||||
_LOGGER.error(f"Error translating login response to json")
|
||||
return
|
||||
|
||||
if json_data is not None:
|
||||
if json_data[0]["code"] == 0:
|
||||
self._token = json_data[0]["value"]["Token"]["name"]
|
||||
_LOGGER.info(f"Reolink camera logged in at IP {self._ip}")
|
||||
else:
|
||||
_LOGGER.error(f"Failed to login at IP {self._ip}. No token available")
|
||||
else:
|
||||
_LOGGER.error(f"Failed to login at IP {self._ip}. Connection error.")
|
||||
|
||||
def logout(self):
|
||||
body = [{"cmd":"Logout","action":0,"param":{}}]
|
||||
param = {"cmd": "Logout", "token": self._token}
|
||||
|
||||
self.send(body, param)
|
||||
|
||||
def set_ftp(self, enabled):
|
||||
self.status()
|
||||
|
||||
if not self._ftp_settings:
|
||||
_LOGGER.error("Error while fetching current FTP settings")
|
||||
return
|
||||
|
||||
if enabled == True:
|
||||
newValue = 1
|
||||
else:
|
||||
newValue = 0
|
||||
|
||||
body = [{"cmd":"SetFtp","action":0,"param": self._ftp_settings["value"] }]
|
||||
body[0]["param"]["Ftp"]["schedule"]["enable"] = newValue
|
||||
|
||||
response = self.send(body, {"cmd": "SetFtp", "token": self._token} )
|
||||
try:
|
||||
json_data = json.loads(response.text)
|
||||
if json_data[0]["value"]["rspCode"] == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
_LOGGER.error(f"Error translating FTP response to json")
|
||||
return False
|
||||
|
||||
def set_email(self, enabled):
|
||||
self.status()
|
||||
|
||||
if not self._email_settings:
|
||||
_LOGGER.error("Error while fetching current email settings")
|
||||
return
|
||||
|
||||
if enabled == True:
|
||||
newValue = 1
|
||||
else:
|
||||
newValue = 0
|
||||
|
||||
body = [{"cmd":"SetEmail","action":0,"param": self._email_settings["value"] }]
|
||||
body[0]["param"]["Email"]["schedule"]["enable"] = newValue
|
||||
|
||||
response = self.send(body, {"cmd": "SetEmail", "token": self._token} )
|
||||
try:
|
||||
json_data = json.loads(response.text)
|
||||
if json_data[0]["value"]["rspCode"] == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
_LOGGER.error(f"Error translating Email response to json")
|
||||
return False
|
||||
|
||||
def set_ir_lights(self, enabled):
|
||||
self.status()
|
||||
|
||||
if not self._ir_settings:
|
||||
_LOGGER.error("Error while fetching current IR light settings")
|
||||
return
|
||||
|
||||
if enabled == True:
|
||||
newValue = "Auto"
|
||||
else:
|
||||
newValue = "Off"
|
||||
|
||||
body = [{"cmd":"SetIrLights","action":0,"param": self._ir_settings["value"] }]
|
||||
body[0]["param"]["IrLights"]["state"] = newValue
|
||||
|
||||
response = self.send(body, {"cmd": "SetIrLights", "token": self._token} )
|
||||
try:
|
||||
json_data = json.loads(response.text)
|
||||
if json_data[0]["value"]["rspCode"] == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error(f"Error translating IR Lights response to json")
|
||||
return False
|
||||
|
||||
def send(self, body, param, stream=False):
|
||||
try:
|
||||
if (self._token is None and
|
||||
(body is None or body[0]["cmd"] != "Login")):
|
||||
_LOGGER.info(f"Reolink camera at IP {self._ip} is not logged in")
|
||||
return
|
||||
|
||||
if body is None:
|
||||
response = requests.get(self._url, params=param, stream=stream)
|
||||
else:
|
||||
response = requests.post(self._url, data=json.dumps(body), params=param)
|
||||
|
||||
return response
|
||||
except Exception:
|
||||
_LOGGER.error(f"Exception while calling Reolink camera API at ip {self._ip}")
|
||||
return None
|
||||
|
@ -1 +1,160 @@
|
||||
"""Reolink Camera component for HomeAssistant."""
|
||||
"""Reolink integration for HomeAssistant."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_TIMEOUT,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .base import ReolinkBase, ReolinkPush
|
||||
from .const import (
|
||||
BASE,
|
||||
CONF_CHANNEL,
|
||||
CONF_MOTION_OFF_DELAY,
|
||||
CONF_PLAYBACK_MONTHS,
|
||||
CONF_PLAYBACK_THUMBNAILS,
|
||||
CONF_PROTOCOL,
|
||||
CONF_STREAM,
|
||||
CONF_THUMBNAIL_OFFSET,
|
||||
COORDINATOR,
|
||||
DEFAULT_PLAYBACK_THUMBNAILS,
|
||||
DEFAULT_THUMBNAIL_OFFSET,
|
||||
DOMAIN,
|
||||
EVENT_DATA_RECEIVED,
|
||||
PUSH_MANAGER,
|
||||
SERVICE_PTZ_CONTROL,
|
||||
SERVICE_SET_DAYNIGHT,
|
||||
SERVICE_SET_SENSITIVITY,
|
||||
)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["camera", "switch", "binary_sensor"]
|
||||
|
||||
|
||||
async def async_setup(
|
||||
hass: HomeAssistant, config: dict
|
||||
): # pylint: disable=unused-argument
|
||||
"""Set up the Reolink component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Reolink from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
base = ReolinkBase(hass, entry.data, entry.options)
|
||||
base.sync_functions.append(entry.add_update_listener(update_listener))
|
||||
|
||||
if not await base.connect_api():
|
||||
return False
|
||||
hass.data[DOMAIN][entry.entry_id] = {BASE: base}
|
||||
|
||||
try:
|
||||
"""Get a push manager, there should be one push manager per mac address"""
|
||||
push = hass.data[DOMAIN][base.push_manager]
|
||||
except KeyError:
|
||||
push = ReolinkPush(
|
||||
hass,
|
||||
base.api.host,
|
||||
base.api.onvif_port,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
await push.subscribe(base.event_id)
|
||||
hass.data[DOMAIN][base.push_manager] = push
|
||||
|
||||
async def async_update_data():
|
||||
"""Perform the actual updates."""
|
||||
|
||||
async with async_timeout.timeout(base.timeout):
|
||||
await push.renew()
|
||||
await base.update_states()
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="reolink",
|
||||
update_method=async_update_data,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_refresh()
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id][COORDINATOR] = coordinator
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, base.stop())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Update the configuration at the base entity and API."""
|
||||
base: ReolinkBase = hass.data[DOMAIN][entry.entry_id][BASE]
|
||||
|
||||
base.motion_off_delay = entry.options[CONF_MOTION_OFF_DELAY]
|
||||
base.playback_months = entry.options[CONF_PLAYBACK_MONTHS]
|
||||
base.playback_thumbnails = entry.options.get(
|
||||
CONF_PLAYBACK_THUMBNAILS, DEFAULT_PLAYBACK_THUMBNAILS
|
||||
)
|
||||
base.playback_thumbnail_offset = entry.options.get(
|
||||
CONF_THUMBNAIL_OFFSET, DEFAULT_THUMBNAIL_OFFSET
|
||||
)
|
||||
|
||||
await base.set_timeout(entry.options[CONF_TIMEOUT])
|
||||
await base.set_protocol(entry.options[CONF_PROTOCOL])
|
||||
await base.set_stream(entry.options[CONF_STREAM])
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
base = hass.data[DOMAIN][entry.entry_id][BASE]
|
||||
push = hass.data[DOMAIN][base.push_manager]
|
||||
|
||||
if not await push.count_members() > 1:
|
||||
await push.unsubscribe()
|
||||
hass.data[DOMAIN].pop(base.push_manager)
|
||||
|
||||
await base.stop()
|
||||
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
if len(hass.data[DOMAIN]) == 0:
|
||||
hass.services.async_remove(DOMAIN, SERVICE_PTZ_CONTROL)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SET_DAYNIGHT)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_SET_SENSITIVITY)
|
||||
|
||||
return unload_ok
|
||||
|
366
custom_components/reolink_dev/base.py
Normal file
@ -0,0 +1,366 @@
|
||||
"""This component updates the camera API and subscription."""
|
||||
import logging
|
||||
import re
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_TIMEOUT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.entity_registry import (
|
||||
async_entries_for_config_entry,
|
||||
async_get_registry as async_get_entity_registry,
|
||||
)
|
||||
|
||||
from reolink.camera_api import Api
|
||||
from reolink.subscription_manager import Manager
|
||||
|
||||
from .const import (
|
||||
BASE,
|
||||
CONF_PLAYBACK_MONTHS,
|
||||
CONF_PLAYBACK_THUMBNAILS,
|
||||
CONF_THUMBNAIL_OFFSET,
|
||||
DEFAULT_PLAYBACK_MONTHS,
|
||||
DEFAULT_PLAYBACK_THUMBNAILS,
|
||||
DEFAULT_THUMBNAIL_OFFSET,
|
||||
EVENT_DATA_RECEIVED,
|
||||
CONF_CHANNEL,
|
||||
CONF_MOTION_OFF_DELAY,
|
||||
CONF_PROTOCOL,
|
||||
CONF_STREAM,
|
||||
DEFAULT_CHANNEL,
|
||||
DEFAULT_MOTION_OFF_DELAY,
|
||||
DEFAULT_PROTOCOL,
|
||||
DEFAULT_STREAM,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
PUSH_MANAGER,
|
||||
SESSION_RENEW_THRESHOLD,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReolinkBase:
|
||||
"""The implementation of the Reolink IP base class."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config: dict, options: dict
|
||||
): # pylint: disable=too-many-arguments
|
||||
"""Initialize a Reolink camera."""
|
||||
|
||||
self._username = config[CONF_USERNAME]
|
||||
self._password = config[CONF_PASSWORD]
|
||||
|
||||
if CONF_CHANNEL not in config:
|
||||
self._channel = DEFAULT_CHANNEL
|
||||
else:
|
||||
self._channel = config[CONF_CHANNEL]
|
||||
|
||||
if CONF_TIMEOUT not in options:
|
||||
self._timeout = DEFAULT_TIMEOUT
|
||||
else:
|
||||
self._timeout = options[CONF_TIMEOUT]
|
||||
|
||||
if CONF_STREAM not in options:
|
||||
self._stream = DEFAULT_STREAM
|
||||
else:
|
||||
self._stream = options[CONF_STREAM]
|
||||
|
||||
if CONF_PROTOCOL not in options:
|
||||
self._protocol = DEFAULT_PROTOCOL
|
||||
else:
|
||||
self._protocol = options[CONF_PROTOCOL]
|
||||
|
||||
self._api = Api(
|
||||
config[CONF_HOST],
|
||||
config[CONF_PORT],
|
||||
self._username,
|
||||
self._password,
|
||||
channel=self._channel - 1,
|
||||
stream=self._stream,
|
||||
protocol=self._protocol,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
|
||||
self._hass = hass
|
||||
self.sync_functions = list()
|
||||
self.motion_detection_state = True
|
||||
|
||||
if CONF_MOTION_OFF_DELAY not in options:
|
||||
self.motion_off_delay = DEFAULT_MOTION_OFF_DELAY
|
||||
else:
|
||||
self.motion_off_delay = options[CONF_MOTION_OFF_DELAY]
|
||||
|
||||
if CONF_PLAYBACK_MONTHS not in options:
|
||||
self.playback_months = DEFAULT_PLAYBACK_MONTHS
|
||||
else:
|
||||
self.playback_months = options[CONF_PLAYBACK_MONTHS]
|
||||
|
||||
if CONF_PLAYBACK_THUMBNAILS not in options:
|
||||
self.playback_thumbnails = DEFAULT_PLAYBACK_THUMBNAILS
|
||||
else:
|
||||
self.playback_thumbnails = options[CONF_PLAYBACK_THUMBNAILS]
|
||||
|
||||
if CONF_THUMBNAIL_OFFSET not in options:
|
||||
self.playback_thumbnail_offset = DEFAULT_THUMBNAIL_OFFSET
|
||||
else:
|
||||
self.playback_thumbnail_offset = options[CONF_THUMBNAIL_OFFSET]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Create the device name."""
|
||||
return self._api.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Create the unique ID, base for all entities."""
|
||||
id = self._api.mac_address.replace(":", "")
|
||||
return f"{id}-{self.channel}"
|
||||
|
||||
@property
|
||||
def event_id(self):
|
||||
"""Create the event ID string."""
|
||||
event_id = self._api.mac_address.replace(":", "")
|
||||
return f"{EVENT_DATA_RECEIVED}-{event_id}"
|
||||
|
||||
@property
|
||||
def push_manager(self):
|
||||
"""Create the event ID string."""
|
||||
push_id = self._api.mac_address.replace(":", "")
|
||||
return f"{PUSH_MANAGER}-{push_id}"
|
||||
|
||||
@property
|
||||
def timeout(self):
|
||||
"""Return the timeout setting."""
|
||||
return self._timeout
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
"""Return the channel setting."""
|
||||
return self._channel
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
"""Return the API object."""
|
||||
return self._api
|
||||
|
||||
async def connect_api(self):
|
||||
"""Connect to the Reolink API and fetch initial dataset."""
|
||||
if not await self._api.get_settings():
|
||||
return False
|
||||
if not await self._api.get_states():
|
||||
return False
|
||||
|
||||
await self._api.is_admin()
|
||||
return True
|
||||
|
||||
async def set_channel(self, channel):
|
||||
"""Set the API channel."""
|
||||
self._channel = channel
|
||||
await self._api.set_channel(channel - 1)
|
||||
|
||||
async def set_protocol(self, protocol):
|
||||
"""Set the protocol."""
|
||||
self._protocol = protocol
|
||||
await self._api.set_protocol(protocol)
|
||||
|
||||
async def set_stream(self, stream):
|
||||
"""Set the stream."""
|
||||
self._stream = stream
|
||||
await self._api.set_stream(stream)
|
||||
|
||||
async def set_timeout(self, timeout):
|
||||
"""Set the API timeout."""
|
||||
self._timeout = timeout
|
||||
await self._api.set_timeout(timeout)
|
||||
|
||||
async def update_states(self):
|
||||
"""Call the API of the camera device to update the states."""
|
||||
await self._api.get_states()
|
||||
|
||||
async def update_settings(self):
|
||||
"""Call the API of the camera device to update the settings."""
|
||||
await self._api.get_settings()
|
||||
|
||||
async def disconnect_api(self):
|
||||
"""Disconnect from the API, so the connection will be released."""
|
||||
await self._api.logout()
|
||||
|
||||
async def stop(self):
|
||||
"""Disconnect the API and deregister the event listener."""
|
||||
await self.disconnect_api()
|
||||
for func in self.sync_functions:
|
||||
await self._hass.async_add_executor_job(func)
|
||||
|
||||
|
||||
class ReolinkPush:
|
||||
"""The implementation of the Reolink IP base class."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, host, port, username, password
|
||||
): # pylint: disable=too-many-arguments
|
||||
"""Initialize a Reolink camera."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._hass = hass
|
||||
|
||||
self._sman = None
|
||||
self._webhook_url = None
|
||||
self._webhook_id = None
|
||||
self._event_id = None
|
||||
|
||||
@property
|
||||
def sman(self):
|
||||
"""Return the session manager object."""
|
||||
return self._sman
|
||||
|
||||
async def subscribe(self, event_id):
|
||||
"""Subscribe to motion events and set the webhook as callback."""
|
||||
self._event_id = event_id
|
||||
self._webhook_id = await self.register_webhook()
|
||||
self._webhook_url = "{}{}".format(
|
||||
get_url(self._hass, prefer_external=False),
|
||||
self._hass.components.webhook.async_generate_path(self._webhook_id),
|
||||
)
|
||||
|
||||
self._sman = Manager(self._host, self._port, self._username, self._password)
|
||||
if await self._sman.subscribe(self._webhook_url):
|
||||
_LOGGER.info(
|
||||
"Host %s subscribed successfully to webhook %s",
|
||||
self._host,
|
||||
self._webhook_url,
|
||||
)
|
||||
await self.set_available(True)
|
||||
else:
|
||||
await self.set_available(False)
|
||||
return True
|
||||
|
||||
async def register_webhook(self):
|
||||
"""
|
||||
Register a webhook for motion events if it does not exist yet (in case of NVR).
|
||||
The webhook name (in info) contains the event id (contains mac address op the camera).
|
||||
So when motion triggers the webhook, it triggers this event. The event is handled by
|
||||
the binary sensor, in case of NVR the binary sensor also figures out what channel has
|
||||
the motion. So the flow is: camera onvif event->webhook->HA event->binary sensor.
|
||||
"""
|
||||
_LOGGER.debug("Registering webhook for event ID %s", self._event_id)
|
||||
|
||||
webhook_id = self._hass.components.webhook.async_generate_id()
|
||||
self._hass.components.webhook.async_register(
|
||||
DOMAIN, self._event_id, webhook_id, handle_webhook
|
||||
)
|
||||
|
||||
return webhook_id
|
||||
|
||||
async def renew(self):
|
||||
"""Renew the subscription of the motion events (lease time is set to 15 minutes)."""
|
||||
if self._sman.renewtimer <= SESSION_RENEW_THRESHOLD:
|
||||
if not await self._sman.renew():
|
||||
_LOGGER.error(
|
||||
"Host %s error renewing the Reolink subscription",
|
||||
self._host,
|
||||
)
|
||||
await self.set_available(False)
|
||||
await self._sman.subscribe(self._webhook_url)
|
||||
else:
|
||||
await self.set_available(True)
|
||||
else:
|
||||
await self.set_available(True)
|
||||
|
||||
async def set_available(self, available: bool):
|
||||
"""Set the availability state to the base object."""
|
||||
self._hass.bus.async_fire(self._event_id, {"available": available})
|
||||
|
||||
async def unsubscribe(self):
|
||||
"""Unsubscribe from the motion events."""
|
||||
await self.set_available(False)
|
||||
await self.unregister_webhook()
|
||||
return await self._sman.unsubscribe()
|
||||
|
||||
async def unregister_webhook(self):
|
||||
"""Unregister the webhook for motion events."""
|
||||
|
||||
_LOGGER.debug("Unregistering webhook %s", self._webhook_id)
|
||||
self._hass.components.webhook.async_unregister(self._webhook_id)
|
||||
|
||||
async def count_members(self):
|
||||
"""Count the number of camera's using this push manager."""
|
||||
members = 0
|
||||
for entry_id in self._hass.data[DOMAIN]:
|
||||
_LOGGER.debug("Got data entry: %s", entry_id)
|
||||
|
||||
if PUSH_MANAGER in entry_id:
|
||||
continue # Count config entries only
|
||||
|
||||
try:
|
||||
base = self._hass.data[DOMAIN][entry_id][BASE]
|
||||
if base.event_id == self._event_id:
|
||||
members += 1
|
||||
except AttributeError:
|
||||
pass
|
||||
except KeyError:
|
||||
pass
|
||||
_LOGGER.debug("Found %d listeners for event %s", members, self._event_id)
|
||||
return members
|
||||
|
||||
|
||||
async def handle_webhook(hass, webhook_id, request):
|
||||
"""Handle incoming webhook from Reolink for inbound messages and calls."""
|
||||
_LOGGER.debug("Reolink webhook triggered")
|
||||
|
||||
if not request.body_exists:
|
||||
_LOGGER.debug("Webhook triggered without payload")
|
||||
|
||||
data = await request.text()
|
||||
if not data:
|
||||
_LOGGER.debug("Webhook triggered with unknown payload")
|
||||
return
|
||||
|
||||
_LOGGER.debug(data)
|
||||
matches = re.findall(r'Name="IsMotion" Value="(.+?)"', data)
|
||||
if matches:
|
||||
is_motion = matches[0] == "true"
|
||||
else:
|
||||
_LOGGER.debug("Webhook triggered with unknown payload")
|
||||
return
|
||||
|
||||
event_id = await get_event_by_webhook(hass, webhook_id)
|
||||
if not event_id:
|
||||
_LOGGER.error("Webhook triggered without event to fire")
|
||||
|
||||
hass.bus.async_fire(event_id, {"motion": is_motion})
|
||||
|
||||
|
||||
async def get_webhook_by_event(hass: HomeAssistant, event_id):
|
||||
"""Find the webhook_id by the event_id."""
|
||||
try:
|
||||
handlers = hass.data["webhook"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
for wid, info in handlers.items():
|
||||
_LOGGER.debug("Webhook: %s", wid)
|
||||
_LOGGER.debug(info)
|
||||
if info["name"] == event_id:
|
||||
return wid
|
||||
|
||||
|
||||
async def get_event_by_webhook(hass: HomeAssistant, webhook_id):
|
||||
"""Find the event_id by the webhook_id."""
|
||||
try:
|
||||
handlers = hass.data["webhook"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
for wid, info in handlers.items():
|
||||
if wid == webhook_id:
|
||||
event_id = info["name"]
|
||||
return event_id
|
106
custom_components/reolink_dev/binary_sensor.py
Normal file
@ -0,0 +1,106 @@
|
||||
"""This component provides support for Reolink motion events."""
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
|
||||
from .const import EVENT_DATA_RECEIVED
|
||||
from .entity import ReolinkEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DEVICE_CLASS = "motion"
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Reolink IP Camera switches."""
|
||||
sensor = MotionSensor(hass, config_entry)
|
||||
async_add_devices([sensor], update_before_add=False)
|
||||
|
||||
|
||||
class MotionSensor(ReolinkEntity, BinarySensorEntity):
|
||||
"""An implementation of a Reolink IP camera motion sensor."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a the switch."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
BinarySensorEntity.__init__(self)
|
||||
|
||||
self._available = False
|
||||
self._event_state = False
|
||||
self._last_motion = datetime.datetime.min
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_motion_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return f"{self._base.name} motion"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
if not self._base.motion_detection_state:
|
||||
self._state = False
|
||||
return self._state
|
||||
|
||||
if self._event_state or self._base.motion_off_delay == 0:
|
||||
self._state = self._event_state
|
||||
return self._state
|
||||
|
||||
if (
|
||||
datetime.datetime.now() - self._last_motion
|
||||
).total_seconds() < self._base.motion_off_delay:
|
||||
self._state = True
|
||||
else:
|
||||
self._state = False
|
||||
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device."""
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Entity created."""
|
||||
await super().async_added_to_hass()
|
||||
self.hass.bus.async_listen(self._base.event_id, self.handle_event)
|
||||
|
||||
async def handle_event(self, event):
|
||||
"""Handle incoming event for motion detection and availability."""
|
||||
try:
|
||||
self._available = event.data["available"]
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not self._available:
|
||||
return
|
||||
|
||||
try:
|
||||
self._event_state = event.data["motion"]
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if self._base.api.channels > 1:
|
||||
# Pull the motion state for the NVR channel, it has only 1 event
|
||||
self._event_state = await self._base.api.get_motion_state()
|
||||
|
||||
if self._event_state:
|
||||
self._last_motion = datetime.datetime.now()
|
||||
else:
|
||||
if self._base.motion_off_delay > 0:
|
||||
await asyncio.sleep(self._base.motion_off_delay)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
@ -1,174 +1,147 @@
|
||||
"""This component provides basic support for Reolink IP cameras."""
|
||||
import logging
|
||||
"""This component provides support for Reolink IP cameras."""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
import datetime
|
||||
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM, ENTITY_IMAGE_URL
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from custom_components.reolink_dev.ReolinkPyPi.camera import ReolinkApi
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_aiohttp_proxy_web,
|
||||
async_get_clientsession,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
SERVICE_PTZ_CONTROL,
|
||||
SERVICE_SET_BACKLIGHT,
|
||||
SERVICE_SET_DAYNIGHT,
|
||||
SERVICE_SET_SENSITIVITY,
|
||||
)
|
||||
from .entity import ReolinkEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_MOTION = "motion"
|
||||
STATE_NO_MOTION = "no_motion"
|
||||
STATE_IDLE = "idle"
|
||||
|
||||
DEFAULT_NAME = "Reolink Camera"
|
||||
DEFAULT_STREAM = "main"
|
||||
DEFAULT_PROTOCOL = "rtmp"
|
||||
DEFAULT_CHANNEL = 0
|
||||
CONF_STREAM = "stream"
|
||||
CONF_PROTOCOL = "protocol"
|
||||
CONF_CHANNEL = "channel"
|
||||
DOMAIN = "camera"
|
||||
SERVICE_ENABLE_FTP = 'enable_ftp'
|
||||
SERVICE_DISABLE_FTP = 'disable_ftp'
|
||||
SERVICE_ENABLE_EMAIL = 'enable_email'
|
||||
SERVICE_DISABLE_EMAIL = 'disable_email'
|
||||
SERVICE_ENABLE_IR_LIGHTS = 'enable_ir_lights'
|
||||
SERVICE_DISABLE_IR_LIGHTS = 'disable_ir_lights'
|
||||
|
||||
DEFAULT_BRAND = 'Reolink'
|
||||
DOMAIN_DATA = 'reolink_devices'
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_STREAM, default=DEFAULT_STREAM): vol.In(["main", "sub"]),
|
||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.In(["rtmp", "rtsp"]),
|
||||
vol.Optional(CONF_CHANNEL, default=DEFAULT_CHANNEL): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up a Reolink IP Camera."""
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
stream = config.get(CONF_STREAM)
|
||||
protocol = config.get(CONF_PROTOCOL)
|
||||
channel = config.get(CONF_CHANNEL)
|
||||
name = config.get(CONF_NAME)
|
||||
platform = entity_platform.current_platform.get()
|
||||
camera = ReolinkCamera(hass, config_entry)
|
||||
|
||||
session = ReolinkApi(host, channel)
|
||||
session.login(username, password)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_SENSITIVITY,
|
||||
{
|
||||
vol.Required("sensitivity"): cv.positive_int,
|
||||
vol.Optional("preset"): cv.positive_int,
|
||||
},
|
||||
SERVICE_SET_SENSITIVITY,
|
||||
)
|
||||
|
||||
async_add_devices([ReolinkCamera(hass, session, host, username, password, stream, protocol, channel, name)], update_before_add=True)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_DAYNIGHT,
|
||||
{
|
||||
vol.Required("mode"): cv.string,
|
||||
},
|
||||
SERVICE_SET_DAYNIGHT,
|
||||
)
|
||||
|
||||
# Event enable FTP
|
||||
def handler_enable_ftp(call):
|
||||
component = hass.data.get(DOMAIN)
|
||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_BACKLIGHT,
|
||||
{
|
||||
vol.Required("mode"): cv.string,
|
||||
},
|
||||
SERVICE_SET_BACKLIGHT,
|
||||
)
|
||||
|
||||
if entity:
|
||||
entity.enable_ftp_upload()
|
||||
hass.services.async_register(DOMAIN, SERVICE_ENABLE_FTP, handler_enable_ftp)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_PTZ_CONTROL,
|
||||
{
|
||||
vol.Required("command"): cv.string,
|
||||
vol.Optional("preset"): cv.positive_int,
|
||||
vol.Optional("speed"): cv.positive_int,
|
||||
},
|
||||
SERVICE_PTZ_CONTROL,
|
||||
)
|
||||
|
||||
# Event disable FTP
|
||||
def handler_disable_ftp(call):
|
||||
component = hass.data.get(DOMAIN)
|
||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
||||
|
||||
if entity:
|
||||
entity.disable_ftp_upload()
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_DISABLE_FTP, handler_disable_ftp)
|
||||
|
||||
# Event enable email
|
||||
def handler_enable_email(call):
|
||||
component = hass.data.get(DOMAIN)
|
||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
||||
|
||||
if entity:
|
||||
entity.enable_email()
|
||||
hass.services.async_register(DOMAIN, SERVICE_ENABLE_EMAIL, handler_enable_email)
|
||||
|
||||
# Event disable email
|
||||
def handler_disable_email(call):
|
||||
component = hass.data.get(DOMAIN)
|
||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
||||
|
||||
if entity:
|
||||
entity.disable_email()
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_DISABLE_EMAIL, handler_disable_email)
|
||||
|
||||
# Event enable ir lights
|
||||
def handler_enable_ir_lights(call):
|
||||
component = hass.data.get(DOMAIN)
|
||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
||||
|
||||
if entity:
|
||||
entity.enable_ir_lights()
|
||||
hass.services.async_register(DOMAIN, SERVICE_ENABLE_IR_LIGHTS, handler_enable_ir_lights)
|
||||
|
||||
# Event disable ir lights
|
||||
def handler_disable_ir_lights(call):
|
||||
component = hass.data.get(DOMAIN)
|
||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
||||
|
||||
if entity:
|
||||
entity.disable_ir_lights()
|
||||
|
||||
hass.services.async_register(DOMAIN, SERVICE_DISABLE_IR_LIGHTS, handler_disable_ir_lights)
|
||||
async_add_devices([camera])
|
||||
|
||||
|
||||
class ReolinkCamera(Camera):
|
||||
class ReolinkCamera(ReolinkEntity, Camera):
|
||||
"""An implementation of a Reolink IP camera."""
|
||||
|
||||
def __init__(self, hass, session, host, username, password, stream, protocol, channel, name):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a Reolink camera."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
Camera.__init__(self)
|
||||
|
||||
super().__init__()
|
||||
self._host = host
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._stream = stream
|
||||
self._protocol = protocol
|
||||
self._channel = channel
|
||||
self._name = name
|
||||
self._reolinkSession = session
|
||||
self._hass = hass
|
||||
self._manager = self._hass.data[DATA_FFMPEG]
|
||||
|
||||
self._last_update = 0
|
||||
self._ffmpeg = self._hass.data[DATA_FFMPEG]
|
||||
self._last_image = None
|
||||
self._last_motion = 0
|
||||
self._ftp_state = None
|
||||
self._email_state = None
|
||||
self._ir_state = None
|
||||
self._ptzpresets = dict()
|
||||
self._state = STATE_IDLE
|
||||
self._ptz_commands = {
|
||||
"AUTO": "Auto",
|
||||
"DOWN": "Down",
|
||||
"FOCUSDEC": "FocusDec",
|
||||
"FOCUSINC": "FocusInc",
|
||||
"LEFT": "Left",
|
||||
"LEFTDOWN": "LeftDown",
|
||||
"LEFTUP": "LeftUp",
|
||||
"RIGHT": "Right",
|
||||
"RIGHTDOWN": "RightDown",
|
||||
"RIGHTUP": "RightUp",
|
||||
"STOP": "Stop",
|
||||
"TOPOS": "ToPos",
|
||||
"UP": "Up",
|
||||
"ZOOMDEC": "ZoomDec",
|
||||
"ZOOMINC": "ZoomInc",
|
||||
}
|
||||
self._daynight_modes = {
|
||||
"AUTO": "Auto",
|
||||
"COLOR": "Color",
|
||||
"BLACKANDWHITE": "Black&White",
|
||||
}
|
||||
|
||||
self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.disconnect)
|
||||
self._backlight_modes = {
|
||||
"BACKLIGHTCONTROL": "BackLightControl",
|
||||
"DYNAMICRANGECONTROL": "DynamicRangeControl",
|
||||
"OFF": "Off",
|
||||
}
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_camera_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._base.name
|
||||
|
||||
@property
|
||||
def ptz_support(self):
|
||||
"""Return whether the camera has PTZ support."""
|
||||
return self._base.api.ptz_support
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the camera state attributes."""
|
||||
attrs = {"access_token": self.access_tokens[-1]}
|
||||
attrs = {}
|
||||
if self._base.api.ptz_support:
|
||||
attrs["ptz_presets"] = self._base.api.ptz_presets
|
||||
|
||||
if self._last_motion:
|
||||
attrs["last_motion"] = self._last_motion
|
||||
|
||||
if self._last_update:
|
||||
attrs["last_update"] = self._last_update
|
||||
for key, value in self._backlight_modes.items():
|
||||
if value == self._base.api.backlight_state:
|
||||
attrs["backlight_state"] = key
|
||||
|
||||
attrs["ftp_enabled"] = self._ftp_state
|
||||
attrs["email_enabled"] = self._email_state
|
||||
attrs["ir_lights_enabled"] = self._ir_state
|
||||
attrs["ptzpresets"] = self._ptzpresets
|
||||
for key, value in self._daynight_modes.items():
|
||||
if value == self._base.api.daynight_state:
|
||||
attrs["daynight_state"] = key
|
||||
|
||||
if self._base.api.sensitivity_presets:
|
||||
attrs["sensitivity"] = self.get_sensitivity_presets()
|
||||
|
||||
return attrs
|
||||
|
||||
@ -177,136 +150,72 @@ class ReolinkCamera(Camera):
|
||||
"""Return supported features."""
|
||||
return SUPPORT_STREAM
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed for the device status."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def ftp_state(self):
|
||||
"""Camera Motion recording Status."""
|
||||
return self._ftp_state
|
||||
|
||||
@property
|
||||
def email_state(self):
|
||||
"""Camera email Status."""
|
||||
return self._email_state
|
||||
|
||||
@property
|
||||
def ptzpresets(self):
|
||||
"""Camera PTZ presets list."""
|
||||
return self._ptzpresets
|
||||
|
||||
async def stream_source(self):
|
||||
"""Return the source of the stream."""
|
||||
if self._protocol == "rtsp":
|
||||
rtspChannel = f"{self._channel+1:02d}"
|
||||
stream_source = f"rtsp://{self._username}:{self._password}@{self._host}:{self._reolinkSession.rtspport}/h264Preview_{rtspChannel}_{self._stream}"
|
||||
else:
|
||||
stream_source = f"rtmp://{self._host}:{self._reolinkSession.rtmpport}/bcs/channel{self._channel}_{self._stream}.bcs?channel={self._channel}&stream=0&user={self._username}&password={self._password}"
|
||||
|
||||
return stream_source
|
||||
return await self._base.api.get_stream_source()
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
stream_source = await self.stream_source()
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self._hass.loop)
|
||||
await stream.open_camera(stream_source)
|
||||
websession = async_get_clientsession(self._hass)
|
||||
stream_coro = websession.get(stream_source, timeout=10)
|
||||
|
||||
try:
|
||||
stream_reader = await stream.get_reader()
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self._hass,
|
||||
request,
|
||||
stream_reader,
|
||||
self._manager.ffmpeg_stream_content_type,
|
||||
)
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
return self._reolinkSession.still_image
|
||||
return await async_aiohttp_proxy_web(self._hass, request, stream_coro)
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
return self._reolinkSession.snapshot
|
||||
return await self._base.api.get_snapshot()
|
||||
|
||||
def enable_ftp_upload(self):
|
||||
"""Enable motion ftp recording in camera."""
|
||||
if self._reolinkSession.set_ftp(True):
|
||||
self._ftp_state = True
|
||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
||||
async def ptz_control(self, command, **kwargs):
|
||||
"""Pass PTZ command to the camera."""
|
||||
if not self.ptz_support:
|
||||
_LOGGER.error("PTZ is not supported on this device")
|
||||
return
|
||||
|
||||
def disable_ftp_upload(self):
|
||||
"""Disable motion ftp recording."""
|
||||
if self._reolinkSession.set_ftp(False):
|
||||
self._ftp_state = False
|
||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
||||
await self._base.api.set_ptz_command(
|
||||
command=self._ptz_commands[command], **kwargs
|
||||
)
|
||||
|
||||
def enable_email(self):
|
||||
"""Enable email motion detection in camera."""
|
||||
if self._reolinkSession.set_email(True):
|
||||
self._email_state = True
|
||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
||||
def get_sensitivity_presets(self):
|
||||
"""Get formatted sensitivity presets."""
|
||||
presets = list()
|
||||
preset = dict()
|
||||
|
||||
def disable_email(self):
|
||||
"""Disable email motion detection."""
|
||||
if self._reolinkSession.set_email(False):
|
||||
self._email_state = False
|
||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
||||
for api_preset in self._base.api.sensitivity_presets:
|
||||
preset["id"] = api_preset["id"]
|
||||
preset["sensitivity"] = api_preset["sensitivity"]
|
||||
|
||||
def enable_ir_lights(self):
|
||||
"""Enable IR lights."""
|
||||
if self._reolinkSession.set_ir_lights(True):
|
||||
self._ir_state = True
|
||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
||||
time_string = f'{api_preset["beginHour"]}:{api_preset["beginMin"]}'
|
||||
begin = datetime.strptime(time_string, "%H:%M")
|
||||
preset["begin"] = begin.strftime("%H:%M")
|
||||
|
||||
def disable_ir_lights(self):
|
||||
"""Disable IR lights."""
|
||||
if self._reolinkSession.set_ir_lights(False):
|
||||
self._ir_state = False
|
||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
||||
time_string = f'{api_preset["endHour"]}:{api_preset["endMin"]}'
|
||||
end = datetime.strptime(time_string, "%H:%M")
|
||||
preset["end"] = end.strftime("%H:%M")
|
||||
|
||||
async def update_motion_state(self):
|
||||
if self._reolinkSession.motion_state == True:
|
||||
self._state = STATE_MOTION
|
||||
self._last_motion = self._reolinkSession.last_motion
|
||||
else:
|
||||
self._state = STATE_NO_MOTION
|
||||
|
||||
async def update_status(self):
|
||||
self._reolinkSession.status()
|
||||
presets.append(preset.copy())
|
||||
|
||||
self._last_update = datetime.datetime.now()
|
||||
self._ftp_state = self._reolinkSession.ftp_state
|
||||
self._email_state = self._reolinkSession.email_state
|
||||
self._ir_state = self._reolinkSession.ir_state
|
||||
self._ptzpresets = self._reolinkSession.ptzpresets
|
||||
return presets
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the camera."""
|
||||
try:
|
||||
self._hass.loop.create_task(self.update_motion_state())
|
||||
async def set_sensitivity(self, sensitivity, **kwargs):
|
||||
"""Set the sensitivity to the camera."""
|
||||
if "preset" in kwargs:
|
||||
kwargs["preset"] += 1 # The camera preset ID's on the GUI are always +1
|
||||
await self._base.api.set_sensitivity(value=sensitivity, **kwargs)
|
||||
|
||||
if (self._last_update == 0 or
|
||||
(datetime.datetime.now() - self._last_update).total_seconds() >= 30):
|
||||
self._hass.loop.create_task(self.update_status())
|
||||
async def set_daynight(self, mode):
|
||||
"""Set the day and night mode to the camera."""
|
||||
await self._base.api.set_daynight(value=self._daynight_modes[mode])
|
||||
|
||||
except Exception as ex:
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
async def set_backlight(self, mode):
|
||||
"""Set the backlight mode to the camera."""
|
||||
await self._base.api.set_backlight(value=self._backlight_modes[mode])
|
||||
|
||||
def disconnect(self, event):
|
||||
_LOGGER.info("Disconnecting from Reolink camera")
|
||||
self._reolinkSession.logout()
|
||||
async def async_enable_motion_detection(self):
|
||||
"""Predefined camera service implementation."""
|
||||
self._base.motion_detection_state = True
|
||||
|
||||
async def async_disable_motion_detection(self):
|
||||
"""Predefined camera service implementation."""
|
||||
self._base.motion_detection_state = False
|
||||
|
217
custom_components/reolink_dev/config_flow.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Config flow for the Reolink camera component."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, data_entry_flow, exceptions
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_TIMEOUT,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .base import ReolinkBase
|
||||
from .const import (
|
||||
BASE,
|
||||
CONF_CHANNEL,
|
||||
CONF_MOTION_OFF_DELAY,
|
||||
CONF_PLAYBACK_MONTHS,
|
||||
CONF_PLAYBACK_THUMBNAILS,
|
||||
CONF_PROTOCOL,
|
||||
CONF_STREAM,
|
||||
CONF_THUMBNAIL_OFFSET,
|
||||
DEFAULT_MOTION_OFF_DELAY,
|
||||
DEFAULT_PLAYBACK_MONTHS,
|
||||
DEFAULT_PLAYBACK_THUMBNAILS,
|
||||
DEFAULT_PROTOCOL,
|
||||
DEFAULT_STREAM,
|
||||
DEFAULT_THUMBNAIL_OFFSET,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Reolink camera's."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
channels = 1
|
||||
mac_address = None
|
||||
base = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return ReolinkOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.data = user_input
|
||||
|
||||
try:
|
||||
self.info = await self.async_validate_input(self.hass, user_input)
|
||||
|
||||
if self.channels > 1:
|
||||
return await self.async_step_nvr()
|
||||
|
||||
self.data[CONF_CHANNEL] = 1
|
||||
await self.async_set_unique_id(
|
||||
f"{self.mac_address}{user_input[CONF_CHANNEL]}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=self.info["title"], data=self.data)
|
||||
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidHost:
|
||||
errors["host"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Optional(CONF_PORT, default=80): cv.positive_int,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_nvr(self, user_input=None):
|
||||
"""Configure a NVR with multiple channels."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self.data.update(user_input)
|
||||
|
||||
await self.async_set_unique_id(
|
||||
f"{self.mac_address}{user_input[CONF_CHANNEL]}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
await self.base.set_channel(user_input[CONF_CHANNEL])
|
||||
await self.base.update_settings()
|
||||
|
||||
return self.async_create_entry(title=self.base.name, data=self.data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="nvr",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CHANNEL): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=self.channels)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_validate_input(self, hass: core.HomeAssistant, user_input: dict):
|
||||
"""Validate the user input allows us to connect."""
|
||||
self.base = ReolinkBase(hass, user_input, [])
|
||||
|
||||
if not await self.base.connect_api():
|
||||
raise CannotConnect
|
||||
|
||||
title = self.base.api.name
|
||||
self.channels = self.base.api.channels
|
||||
self.mac_address = self.base.api.mac_address
|
||||
|
||||
return {"title": title}
|
||||
|
||||
async def async_finish_flow(self, flow, result):
|
||||
"""Finish flow."""
|
||||
# if result['type'] == data_entry_flow.RESULT_TYPE_ABORT:
|
||||
self.base.disconnect_api()
|
||||
|
||||
|
||||
class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle Reolink options."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize Reolink options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None): # pylint: disable=unused-argument
|
||||
"""Manage the Reolink options."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_PROTOCOL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_PROTOCOL, DEFAULT_PROTOCOL
|
||||
),
|
||||
): vol.In(["rtmp", "rtsp"]),
|
||||
vol.Required(
|
||||
CONF_STREAM,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_STREAM, DEFAULT_STREAM
|
||||
),
|
||||
): vol.In(["main", "sub"]),
|
||||
vol.Required(
|
||||
CONF_MOTION_OFF_DELAY,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_MOTION_OFF_DELAY, DEFAULT_MOTION_OFF_DELAY
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
vol.Required(
|
||||
CONF_PLAYBACK_MONTHS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_PLAYBACK_MONTHS, DEFAULT_PLAYBACK_MONTHS
|
||||
),
|
||||
): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_PLAYBACK_THUMBNAILS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_PLAYBACK_THUMBNAILS, DEFAULT_PLAYBACK_THUMBNAILS
|
||||
),
|
||||
): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_THUMBNAIL_OFFSET,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_THUMBNAIL_OFFSET, DEFAULT_THUMBNAIL_OFFSET
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=0, max=60)),
|
||||
vol.Optional(
|
||||
CONF_TIMEOUT,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_TIMEOUT, DEFAULT_TIMEOUT
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AlreadyConfigured(exceptions.HomeAssistantError):
|
||||
"""Error to indicate device is already configured."""
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidHost(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is an invalid hostname."""
|
31
custom_components/reolink_dev/const.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Constants for the Reolink Camera integration."""
|
||||
|
||||
DOMAIN = "reolink_dev"
|
||||
DOMAIN_DATA = "reolink_dev_devices"
|
||||
EVENT_DATA_RECEIVED = "reolink_dev-event"
|
||||
COORDINATOR = "coordinator"
|
||||
BASE = "base"
|
||||
PUSH_MANAGER = "push_manager"
|
||||
SESSION_RENEW_THRESHOLD = 300
|
||||
|
||||
CONF_STREAM = "stream"
|
||||
CONF_PROTOCOL = "protocol"
|
||||
CONF_CHANNEL = "channel"
|
||||
CONF_MOTION_OFF_DELAY = "motion_off_delay"
|
||||
CONF_PLAYBACK_MONTHS = "playback_months"
|
||||
CONF_PLAYBACK_THUMBNAILS = "playback_thumbnails"
|
||||
CONF_THUMBNAIL_OFFSET = "playback_thumbnail_offset"
|
||||
|
||||
DEFAULT_CHANNEL = 1
|
||||
DEFAULT_MOTION_OFF_DELAY = 60
|
||||
DEFAULT_PROTOCOL = "rtmp"
|
||||
DEFAULT_STREAM = "main"
|
||||
DEFAULT_TIMEOUT = 30
|
||||
DEFAULT_PLAYBACK_MONTHS = 2
|
||||
DEFAULT_PLAYBACK_THUMBNAILS = False
|
||||
DEFAULT_THUMBNAIL_OFFSET = 6
|
||||
|
||||
SERVICE_PTZ_CONTROL = "ptz_control"
|
||||
SERVICE_SET_BACKLIGHT = "set_backlight"
|
||||
SERVICE_SET_DAYNIGHT = "set_daynight"
|
||||
SERVICE_SET_SENSITIVITY = "set_sensitivity"
|
41
custom_components/reolink_dev/entity.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Reolink parent entity class."""
|
||||
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import BASE, COORDINATOR, DOMAIN
|
||||
|
||||
|
||||
class ReolinkEntity(CoordinatorEntity):
|
||||
"""Parent class for Reolink Entities."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize common aspects of a Reolink entity."""
|
||||
coordinator = hass.data[DOMAIN][config.entry_id][COORDINATOR]
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._base = hass.data[DOMAIN][config.entry_id][BASE]
|
||||
self._hass = hass
|
||||
self._state = False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Information about this entity/device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._base.unique_id)},
|
||||
"connections": {(CONNECTION_NETWORK_MAC, self._base.api.mac_address)},
|
||||
"name": self._base.name,
|
||||
"sw_version": self._base.api.sw_version,
|
||||
"model": self._base.api.model,
|
||||
"manufacturer": self._base.api.manufacturer,
|
||||
"channel": self._base.channel
|
||||
}
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._base.api.session_active
|
||||
|
||||
async def request_refresh(self):
|
||||
"""Call the coordinator to update the API."""
|
||||
await self.coordinator.async_request_refresh()
|
@ -1,8 +1,25 @@
|
||||
{
|
||||
"domain": "reolink_dev",
|
||||
"name": "Reolink IP camera",
|
||||
"documentation": "https://www.example.com",
|
||||
"dependencies": ["ffmpeg"],
|
||||
"codeowners": ["@fwestenberg"],
|
||||
"requirements": ["aiosmtpd==1.2"]
|
||||
}
|
||||
"domain": "reolink_dev",
|
||||
"name": "Reolink IP camera",
|
||||
"documentation": "https://github.com/fwestenberg/reolink_dev",
|
||||
"issue_tracker": "https://github.com/fwestenberg/reolink_dev/issues",
|
||||
"version": "0.15",
|
||||
"requirements": [
|
||||
"reolink==0.0.17"
|
||||
],
|
||||
"dependencies": [
|
||||
"ffmpeg",
|
||||
"webhook"
|
||||
],
|
||||
"after_dependencies": [
|
||||
"media_source",
|
||||
"http"
|
||||
],
|
||||
"codeowners": [
|
||||
"@fwestenberg"
|
||||
],
|
||||
"config_flow": true,
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {}
|
||||
}
|
||||
|
412
custom_components/reolink_dev/media_source.py
Normal file
@ -0,0 +1,412 @@
|
||||
"""Reolink Camera Media Source Implementation."""
|
||||
from urllib import parse
|
||||
import secrets
|
||||
import datetime as dt
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
from aiohttp import web
|
||||
from haffmpeg.tools import IMAGE_JPEG
|
||||
|
||||
from dateutil import relativedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
import homeassistant.util.dt as dt_utils
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
# from homeassistant.components.http.auth import async_sign_path
|
||||
from homeassistant.components.media_player.errors import BrowseError
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
MEDIA_TYPE_VIDEO,
|
||||
)
|
||||
from homeassistant.components.media_source.const import MEDIA_MIME_TYPES
|
||||
from homeassistant.components.media_source.error import MediaSourceError, Unresolvable
|
||||
from homeassistant.components.media_source.models import (
|
||||
BrowseMediaSource,
|
||||
MediaSource,
|
||||
MediaSourceItem,
|
||||
PlayMedia,
|
||||
)
|
||||
|
||||
from homeassistant.components.stream import create_stream
|
||||
from homeassistant.components.ffmpeg import async_get_image
|
||||
|
||||
from custom_components.reolink_dev.base import ReolinkBase
|
||||
|
||||
from . import typings
|
||||
|
||||
from .const import BASE, DEFAULT_THUMBNAIL_OFFSET, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
# MIME_TYPE = "rtmp/mp4"
|
||||
# MIME_TYPE = "video/mp4"
|
||||
MIME_TYPE = "application/x-mpegURL"
|
||||
|
||||
NAME = "Reolink IP Camera"
|
||||
|
||||
|
||||
class IncompatibleMediaSource(MediaSourceError):
|
||||
"""Incompatible media source attributes."""
|
||||
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant):
|
||||
"""Set up Reolink media source."""
|
||||
_LOGGER.debug("Creating REOLink Media Source")
|
||||
source = ReolinkSource(hass)
|
||||
hass.http.register_view(ReolinkSourceThumbnailView(hass, source))
|
||||
return source
|
||||
|
||||
|
||||
class ReolinkSource(MediaSource):
|
||||
"""Provide Reolink camera recordings as media sources."""
|
||||
|
||||
name: str = NAME
|
||||
|
||||
def __init__(self, hass: HomeAssistant):
|
||||
"""Initialize Reolink source."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.cache = {}
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve a media item to a playable item."""
|
||||
_, camera_id, event_id = async_parse_identifier(item)
|
||||
cache: typings.MediaSourceCacheEntry = self.cache[camera_id]
|
||||
event = cache["playback_events"][event_id]
|
||||
base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE]
|
||||
url = await base.api.get_vod_source(event["file"])
|
||||
_LOGGER.debug("Load VOD %s", url)
|
||||
stream = create_stream(self.hass, url)
|
||||
stream.add_provider("hls", timeout=600)
|
||||
url: str = stream.endpoint_url("hls")
|
||||
# the media browser seems to have a problem with the master_playlist
|
||||
# ( it does not load the referenced playlist ) so we will just
|
||||
# force the reference playlist instead, this seems to work
|
||||
# though technically wrong
|
||||
url = url.replace("master_", "")
|
||||
_LOGGER.debug("Proxy %s", url)
|
||||
return PlayMedia(url, MIME_TYPE)
|
||||
|
||||
async def async_browse_media(
|
||||
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
|
||||
) -> BrowseMediaSource:
|
||||
"""Browse media."""
|
||||
|
||||
try:
|
||||
source, camera_id, event_id = async_parse_identifier(item)
|
||||
except Unresolvable as err:
|
||||
raise BrowseError(str(err)) from err
|
||||
|
||||
_LOGGER.debug("Browsing %s, %s, %s", source, camera_id, event_id)
|
||||
|
||||
if camera_id and camera_id not in self.cache:
|
||||
raise BrowseError("Camera does not exist.")
|
||||
|
||||
if (
|
||||
event_id
|
||||
and not "/" in event_id
|
||||
and event_id not in self.cache[camera_id]["playback_events"]
|
||||
):
|
||||
raise BrowseError("Event does not exist.")
|
||||
|
||||
return await self._async_browse_media(source, camera_id, event_id, False)
|
||||
|
||||
async def _async_browse_media(
|
||||
self, source: str, camera_id: str, event_id: str = None, no_descend: bool = True
|
||||
) -> BrowseMediaSource:
|
||||
""" actual browse after input validation """
|
||||
event: typings.VodEvent = None
|
||||
cache: typings.MediaSourceCacheEntry = None
|
||||
start_date = None
|
||||
|
||||
if camera_id and camera_id in self.cache:
|
||||
cache = self.cache[camera_id]
|
||||
|
||||
if cache and event_id:
|
||||
if "playback_events" in cache and event_id in cache["playback_events"]:
|
||||
event = cache["playback_events"][event_id]
|
||||
end_date = event["end"]
|
||||
start_date = event["start"]
|
||||
time = start_date.time()
|
||||
duration = end_date - start_date
|
||||
|
||||
title = f"{time} {duration}"
|
||||
else:
|
||||
year, *rest = event_id.split("/", 3)
|
||||
month = rest[0] if len(rest) > 0 else None
|
||||
day = rest[1] if len(rest) > 1 else None
|
||||
|
||||
start_date = dt.datetime.combine(
|
||||
dt.date(
|
||||
int(year), int(month) if month else 1, int(day) if day else 1
|
||||
),
|
||||
dt.time.min,
|
||||
dt_utils.now().tzinfo,
|
||||
)
|
||||
|
||||
title = f"{start_date.date()}"
|
||||
|
||||
path = f"{source}/{camera_id}/{event_id}"
|
||||
else:
|
||||
if cache is None:
|
||||
camera_id = ""
|
||||
title = NAME
|
||||
else:
|
||||
title = cache["name"]
|
||||
|
||||
path = f"{source}/{camera_id}"
|
||||
|
||||
media_class = MEDIA_CLASS_DIRECTORY if event is None else MEDIA_CLASS_VIDEO
|
||||
|
||||
media = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=path,
|
||||
media_class=media_class,
|
||||
media_content_type=MEDIA_TYPE_VIDEO,
|
||||
title=title,
|
||||
can_play=bool(not event is None and event.get("file")),
|
||||
can_expand=event is None,
|
||||
)
|
||||
|
||||
if not event is None and cache.get("playback_thumbnails", False):
|
||||
url = "/api/" + DOMAIN + f"/media_proxy/{camera_id}/{event_id}"
|
||||
|
||||
# TODO : I cannot find a way to get the current user context at this point
|
||||
# so I will have to leave the view as unauthenticated, as a temporary
|
||||
# security measure, I will add a unique token to the event to limit
|
||||
# "exposure"
|
||||
# url = async_sign_path(self.hass, None, url, dt.timedelta(minutes=30))
|
||||
if "token" not in event:
|
||||
event["token"] = secrets.token_hex()
|
||||
media.thumbnail = f"{url}?token={parse.quote_plus(event['token'])}"
|
||||
|
||||
if not media.can_play and not media.can_expand:
|
||||
_LOGGER.debug(
|
||||
"Camera %s with event %s without media url found", camera_id, event_id
|
||||
)
|
||||
raise IncompatibleMediaSource
|
||||
|
||||
if not media.can_expand or no_descend:
|
||||
return media
|
||||
|
||||
media.children = []
|
||||
|
||||
base: ReolinkBase = None
|
||||
|
||||
if cache is None:
|
||||
for entry_id in self.hass.data[DOMAIN]:
|
||||
entry = self.hass.data[DOMAIN][entry_id]
|
||||
if not isinstance(entry, dict) or not BASE in entry:
|
||||
continue
|
||||
base = entry[BASE]
|
||||
camera_id = base.unique_id
|
||||
cache = self.cache.get(camera_id, None)
|
||||
if cache is None:
|
||||
cache = self.cache[camera_id] = {
|
||||
"entry_id": entry_id,
|
||||
"unique_id": base.unique_id,
|
||||
"playback_events": {},
|
||||
}
|
||||
cache["name"] = base.name
|
||||
|
||||
child = await self._async_browse_media(source, camera_id)
|
||||
media.children.append(child)
|
||||
return media
|
||||
|
||||
base = self.hass.data[DOMAIN][cache["entry_id"]][BASE]
|
||||
|
||||
# TODO: the cache is one way so over time it can grow and have invalid
|
||||
# records, the code should be expanded to invalidate/expire
|
||||
# entries
|
||||
|
||||
if base is None:
|
||||
raise BrowseError("Camera does not exist.")
|
||||
|
||||
if not start_date:
|
||||
if (
|
||||
"playback_day_entries" not in cache
|
||||
or cache.get("playback_months", -1) != base.playback_months
|
||||
):
|
||||
end_date = dt_utils.now()
|
||||
start_date = dt.datetime.combine(end_date.date(), dt.time.min)
|
||||
cache["playback_months"] = base.playback_months
|
||||
if cache["playback_months"] > 1:
|
||||
start_date -= relativedelta.relativedelta(
|
||||
months=int(cache["playback_months"])
|
||||
)
|
||||
|
||||
entries = cache["playback_day_entries"] = []
|
||||
|
||||
search, _ = await base.api.send_search(start_date, end_date, True)
|
||||
|
||||
if not search is None:
|
||||
for status in search:
|
||||
year = status["year"]
|
||||
month = status["mon"]
|
||||
for day, flag in enumerate(status["table"], start=1):
|
||||
if flag == "1":
|
||||
entries.append(dt.date(year, month, day))
|
||||
|
||||
entries.sort()
|
||||
else:
|
||||
entries = cache["playback_day_entries"]
|
||||
|
||||
for date in cache["playback_day_entries"]:
|
||||
child = await self._async_browse_media(
|
||||
source, camera_id, f"{date.year}/{date.month}/{date.day}"
|
||||
)
|
||||
media.children.append(child)
|
||||
|
||||
return media
|
||||
|
||||
cache["playback_thumbnails"] = base.playback_thumbnails
|
||||
|
||||
end_date = dt.datetime.combine(
|
||||
start_date.date(), dt.time.max, start_date.tzinfo
|
||||
)
|
||||
|
||||
_, files = await base.api.send_search(start_date, end_date)
|
||||
|
||||
if not files is None:
|
||||
events = cache.setdefault("playback_events", {})
|
||||
|
||||
for file in files:
|
||||
dto = file["EndTime"]
|
||||
end_date = dt.datetime(
|
||||
dto["year"],
|
||||
dto["mon"],
|
||||
dto["day"],
|
||||
dto["hour"],
|
||||
dto["min"],
|
||||
dto["sec"],
|
||||
0,
|
||||
end_date.tzinfo,
|
||||
)
|
||||
dto = file["StartTime"]
|
||||
start_date = dt.datetime(
|
||||
dto["year"],
|
||||
dto["mon"],
|
||||
dto["day"],
|
||||
dto["hour"],
|
||||
dto["min"],
|
||||
dto["sec"],
|
||||
0,
|
||||
end_date.tzinfo,
|
||||
)
|
||||
event_id = str(start_date.timestamp())
|
||||
event = events.setdefault(event_id, {})
|
||||
event["start"] = start_date
|
||||
event["end"] = end_date
|
||||
event["file"] = file["name"]
|
||||
|
||||
child = await self._async_browse_media(source, camera_id, event_id)
|
||||
media.children.append(child)
|
||||
|
||||
return media
|
||||
|
||||
|
||||
class ReolinkSourceThumbnailView(HomeAssistantView):
|
||||
""" Thumbnial view handler """
|
||||
|
||||
url = "/api/" + DOMAIN + "/media_proxy/{camera_id}/{event_id}"
|
||||
name = "api:" + DOMAIN + ":image"
|
||||
requires_auth = False
|
||||
cors_allowed = True
|
||||
|
||||
def __init__(self, hass: HomeAssistant, source: ReolinkSource):
|
||||
"""Initialize media view """
|
||||
|
||||
self.hass = hass
|
||||
self.source = source
|
||||
|
||||
async def get(
|
||||
self, request: web.Request, camera_id: str, event_id: str
|
||||
) -> web.Response:
|
||||
""" start a GET request. """
|
||||
|
||||
if not camera_id or not event_id:
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
cache: typings.MediaSourceCacheEntry = self.source.cache.get(camera_id, None)
|
||||
if cache is None or "playback_events" not in cache:
|
||||
_LOGGER.debug("camera %s not found", camera_id)
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
event = cache["playback_events"].get(event_id, None)
|
||||
if event is None:
|
||||
_LOGGER.debug("camera %s, event %s not found", camera_id, event_id)
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
token = request.query.get("token")
|
||||
if (token and event.get("token") != token) or (
|
||||
not token and not self.requires_auth
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"invalid or missing token %s for camera %s, event %s",
|
||||
token,
|
||||
camera_id,
|
||||
event_id,
|
||||
)
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
_LOGGER.debug("thumbnail %s, %s", camera_id, event_id)
|
||||
|
||||
base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE]
|
||||
|
||||
image = event.get("thumbnail", None)
|
||||
if (
|
||||
image is None
|
||||
or cache.get("playback_thumbnail_offset", DEFAULT_THUMBNAIL_OFFSET)
|
||||
!= base.playback_thumbnail_offset
|
||||
):
|
||||
cache["playback_thumbnails"] = base.playback_thumbnails
|
||||
cache["playback_thumbnail_offset"] = base.playback_thumbnail_offset
|
||||
|
||||
if not cache["playback_thumbnails"]:
|
||||
_LOGGER.debug("Thumbnails not allowed on camera %s", camera_id)
|
||||
raise web.HTTPInternalServerError()
|
||||
|
||||
_LOGGER.debug("generating thumbnail for %s, %s", camera_id, event_id)
|
||||
|
||||
extra_cmd: str = None
|
||||
if cache["playback_thumbnail_offset"] > 0:
|
||||
extra_cmd = f"-ss {cache['playback_thumbnail_offset']}"
|
||||
|
||||
image = event["thumbail"] = await async_get_image(
|
||||
self.hass,
|
||||
await base.api.get_vod_source(event["file"]),
|
||||
extra_cmd=extra_cmd,
|
||||
)
|
||||
_LOGGER.debug("generated thumbnail for %s, %s", camera_id, event_id)
|
||||
|
||||
if image:
|
||||
return web.Response(body=image, content_type=IMAGE_JPEG)
|
||||
|
||||
_LOGGER.debug(
|
||||
"No thumbnail generated for camera %s, event %s", camera_id, event_id
|
||||
)
|
||||
raise web.HTTPInternalServerError()
|
||||
|
||||
|
||||
@callback
|
||||
def async_parse_identifier(
|
||||
item: MediaSourceItem,
|
||||
) -> Tuple[str, str, Optional[str]]:
|
||||
"""Parse identifier."""
|
||||
if not item.identifier:
|
||||
return "events", "", None
|
||||
|
||||
source, path = item.identifier.lstrip("/").split("/", 1)
|
||||
|
||||
if source != "events":
|
||||
raise Unresolvable("Unknown source directory.")
|
||||
|
||||
if "/" in path:
|
||||
camera_id, event_id = path.split("/", 1)
|
||||
|
||||
return source, camera_id, event_id
|
||||
|
||||
return source, path, None
|
@ -1,41 +1,84 @@
|
||||
enable_ftp:
|
||||
description: Enable FTP upload on motion recording.
|
||||
ptz_control:
|
||||
name: Pan/Zoom/Tilt Control
|
||||
description: Execute a PTZ command.
|
||||
target:
|
||||
entity:
|
||||
integration: reolink_dev
|
||||
domain: camera
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the Reolink camera entity to set.
|
||||
description: Name(s) of the Reolink camera entity to execute the command on.
|
||||
example: 'camera.frontdoor'
|
||||
|
||||
disable_ftp:
|
||||
description: Disable FTP upload on motion recording.
|
||||
command:
|
||||
description: >-
|
||||
Command to execute. Possible values are:
|
||||
AUTO DOWN FOCUSDEC FOCUSINC LEFT LEFTDOWN LEFTUP
|
||||
RIGHT RIGHTDOWN RIGHTUP STOP TOPOS UP ZOOMDEC ZOOMINC
|
||||
example: LEFTUP
|
||||
preset:
|
||||
description: (Optional) In case of the command TOPOS. The available presets are listed as attribute on the camera.
|
||||
example: HOME
|
||||
speed:
|
||||
description: (Optional) Speed at which the movement takes place.
|
||||
example: 25
|
||||
|
||||
set_sensitivity:
|
||||
name: Set Motion Sensitivity
|
||||
description: Set the motion detection sensitivity.
|
||||
target:
|
||||
entity:
|
||||
integration: reolink_dev
|
||||
domain: camera
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the Reolink camera entity to set.
|
||||
description: Name(s) of the Reolink camera entity to execute the command on.
|
||||
example: 'camera.frontdoor'
|
||||
|
||||
enable_email:
|
||||
description: Enable email functionality on motion detection.
|
||||
sensitivity:
|
||||
description: New sensitivity, value between 1 (low sensitivity) and 50 (high sensitivity)
|
||||
example: 25
|
||||
preset:
|
||||
description: >-
|
||||
(Optional) Set the sensitivity of a specific preset (time schedule). When no value is supplied,
|
||||
all presets will be changed.
|
||||
|
||||
set_daynight:
|
||||
name: Set Day/Night Mode
|
||||
description: Set day and night parameter.
|
||||
target:
|
||||
entity:
|
||||
integration: reolink_dev
|
||||
domain: camera
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the Reolink camera entity to set.
|
||||
description: Name(s) of the Reolink camera entity to execute the command on.
|
||||
example: 'camera.frontdoor'
|
||||
|
||||
disable_email:
|
||||
description: Disable email functionality on motion detection.
|
||||
mode:
|
||||
description: >-
|
||||
The day and night mode parameter supports the following values:
|
||||
AUTO: Auto switch between black & white mode
|
||||
COLOR: Always record videos in color mode
|
||||
BLACKANDWHITE: Always record videos in black & white mode
|
||||
example: AUTO
|
||||
|
||||
set_backlight:
|
||||
name: Set backlight
|
||||
description: >-
|
||||
Optimizing brightness and contrast levels to compensate for differences
|
||||
between dark and bright objects using either BLC or WDR mode.
|
||||
This may improve image clarity in high contrast situations,
|
||||
but it should be tested at different times of the day and night to ensure there is no negative effect.
|
||||
target:
|
||||
entity:
|
||||
integration: reolink_dev
|
||||
domain: camera
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the Reolink camera entity to set.
|
||||
description: Name(s) of the Reolink camera entity to execute the command on.
|
||||
example: 'camera.frontdoor'
|
||||
|
||||
enable_ir_lights:
|
||||
description: Enable the infrared lights (nightvision) of the Reolink camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the Reolink camera entity to set.
|
||||
example: 'camera.frontdoor'
|
||||
|
||||
disable_ir_lights:
|
||||
description: Disable the infrared lights (nightvision) of the Reolink camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name of the Reolink camera entity to set.
|
||||
example: 'camera.frontdoor'
|
||||
mode:
|
||||
description: >-
|
||||
The backlight parameter supports the following values:
|
||||
BACKLIGHTCONTROL: use Backlight Control
|
||||
DYNAMICRANGECONTROL: use Dynamic Range Control
|
||||
OFF: no optimization
|
||||
example: DYNAMICRANGECONTROL
|
||||
|
42
custom_components/reolink_dev/strings.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Channel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protocol",
|
||||
"stream": "Stream",
|
||||
"timeout": "Timeout",
|
||||
"motion_off_delay": "Motion sensor off delay (seconds)",
|
||||
"playback_months": "Playback range (months)",
|
||||
"playback_thumbnails": "Create thumbnails for playback items",
|
||||
"playback_thumbnail_offset": "Pre-Record offset (seconds) for thumbnail"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
269
custom_components/reolink_dev/switch.py
Normal file
@ -0,0 +1,269 @@
|
||||
"""This component provides support many for Reolink IP cameras switches."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.switch import DEVICE_CLASS_SWITCH
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
from .const import BASE, DOMAIN
|
||||
from .entity import ReolinkEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the Reolink IP Camera switches."""
|
||||
devices = []
|
||||
base = hass.data[DOMAIN][config_entry.entry_id][BASE]
|
||||
|
||||
for capability in await base.api.get_switch_capabilities():
|
||||
if capability == "ftp":
|
||||
devices.append(FTPSwitch(hass, config_entry))
|
||||
elif capability == "email":
|
||||
devices.append(EmailSwitch(hass, config_entry))
|
||||
elif capability == "audio":
|
||||
devices.append(AudioSwitch(hass, config_entry))
|
||||
elif capability == "irLights":
|
||||
devices.append(IRLightsSwitch(hass, config_entry))
|
||||
elif capability == "recording":
|
||||
devices.append(RecordingSwitch(hass, config_entry))
|
||||
else:
|
||||
continue
|
||||
|
||||
async_add_devices(devices, update_before_add=False)
|
||||
|
||||
|
||||
class FTPSwitch(ReolinkEntity, ToggleEntity):
|
||||
"""An implementation of a Reolink IP camera FTP switch."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a Reolink camera."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
ToggleEntity.__init__(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_ftpSwitch_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return f"{self._base.name} FTP"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Camera Motion FTP upload Status."""
|
||||
return self._base.api.ftp_state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Device class of the switch."""
|
||||
return DEVICE_CLASS_SWITCH
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon of the switch."""
|
||||
if self.is_on:
|
||||
return "mdi:folder-upload"
|
||||
|
||||
return "mdi:folder-remove"
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Enable motion ftp recording."""
|
||||
await self._base.api.set_ftp(True)
|
||||
await self.request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Disable motion ftp recording."""
|
||||
await self._base.api.set_ftp(False)
|
||||
await self.request_refresh()
|
||||
|
||||
|
||||
class EmailSwitch(ReolinkEntity, ToggleEntity):
|
||||
"""An implementation of a Reolink IP camera email switch."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a Reolink camera."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
ToggleEntity.__init__(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_emailSwitch_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return f"{self._base.name} email"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Camera Motion email upload Status."""
|
||||
return self._base.api.email_state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Device class of the switch."""
|
||||
return DEVICE_CLASS_SWITCH
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon of the switch."""
|
||||
if self.is_on:
|
||||
return "mdi:email"
|
||||
|
||||
return "mdi:email-outline"
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Enable motion email notification."""
|
||||
await self._base.api.set_email(True)
|
||||
await self.request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Disable motion email notification."""
|
||||
await self._base.api.set_email(False)
|
||||
await self.request_refresh()
|
||||
|
||||
|
||||
class IRLightsSwitch(ReolinkEntity, ToggleEntity):
|
||||
"""An implementation of a Reolink IP camera ir lights switch."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a Reolink camera."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
ToggleEntity.__init__(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_irLightsSwitch_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return f"{self._base.name} IR lights"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Camera Motion ir lights Status."""
|
||||
return self._base.api.ir_state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Device class of the switch."""
|
||||
return DEVICE_CLASS_SWITCH
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon of the switch."""
|
||||
if self.is_on:
|
||||
return "mdi:flashlight"
|
||||
|
||||
return "mdi:flashlight-off"
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Enable motion ir lights."""
|
||||
await self._base.api.set_ir_lights(True)
|
||||
await self.request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Disable motion ir lights."""
|
||||
await self._base.api.set_ir_lights(False)
|
||||
await self.request_refresh()
|
||||
|
||||
|
||||
class RecordingSwitch(ReolinkEntity, ToggleEntity):
|
||||
"""An implementation of a Reolink IP camera recording switch."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a Reolink camera."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
ToggleEntity.__init__(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_recordingSwitch_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return f"{self._base.name} recording"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Camera recording upload Status."""
|
||||
return self._base.api.recording_state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Device class of the switch."""
|
||||
return DEVICE_CLASS_SWITCH
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon of the switch."""
|
||||
if self.is_on:
|
||||
return "mdi:filmstrip"
|
||||
|
||||
return "mdi:filmstrip-off"
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Enable recording."""
|
||||
await self._base.api.set_recording(True)
|
||||
await self.request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Disable recording."""
|
||||
await self._base.api.set_recording(False)
|
||||
await self.request_refresh()
|
||||
|
||||
|
||||
class AudioSwitch(ReolinkEntity, ToggleEntity):
|
||||
"""An implementation of a Reolink IP camera audio switch."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a Reolink camera."""
|
||||
ReolinkEntity.__init__(self, hass, config)
|
||||
ToggleEntity.__init__(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return Unique ID string."""
|
||||
return f"reolink_audioSwitch_{self._base.unique_id}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return f"{self._base.name} record audio"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Camera audio switch Status."""
|
||||
return self._base.api.audio_state
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Device class of the switch."""
|
||||
return DEVICE_CLASS_SWITCH
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon of the switch."""
|
||||
if self.is_on:
|
||||
return "mdi:volume-high"
|
||||
|
||||
return "mdi:volume-off"
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Enable audio recording."""
|
||||
await self._base.api.set_audio(True)
|
||||
await self.request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Disable audio recording."""
|
||||
await self._base.api.set_audio(False)
|
||||
await self.request_refresh()
|
39
custom_components/reolink_dev/translations/de.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Diese Kamera ist bereits konfiguriert"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Verbindung zur Kamera konnte nicht hergestellt werden",
|
||||
"invalid_auth": "Benutzername oder Passwort fehlerhaft",
|
||||
"unknown": "Unerwarteter Fehler"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"username": "Benutzername",
|
||||
"password": "Passwort"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Kanal"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protokoll",
|
||||
"stream": "Stream",
|
||||
"timeout": "Timeout (Sekunden)",
|
||||
"motion_off_delay": "Bewegungssensor Ausschaltverzögerung (Sekunden)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
custom_components/reolink_dev/translations/en.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This camera is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect with the camera",
|
||||
"invalid_auth": "Invalid username or password",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Channel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protocol",
|
||||
"stream": "Stream",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"motion_off_delay": "Motion sensor off delay (seconds)",
|
||||
"playback_months": "Playback range (months)",
|
||||
"playback_thumbnails": "Create thumbnails for playback items",
|
||||
"playback_thumbnail_offset": "Pre-Record offset (seconds) for thumbnail"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
custom_components/reolink_dev/translations/es.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Esta cámara ya ha sido configurada"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "No se pudo conectar con la cámara",
|
||||
"invalid_auth": "Nombre de usuario o contraseña incorrecta",
|
||||
"unknown": "Error inesperado"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Dirección",
|
||||
"port": "Puerto",
|
||||
"username": "Usuario",
|
||||
"password": "Contraseña"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Canal"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protocolo",
|
||||
"stream": "Transferencia",
|
||||
"timeout": "Tiempo fuera (segundos)",
|
||||
"motion_off_delay": "Sensor de movimiento apagado retardo (segundos)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
custom_components/reolink_dev/translations/fr.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Cette caméra est déjà configurée"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Impossible de se connecter à la caméra",
|
||||
"invalid_auth": "Mauvais nom d'utilisateur et/ou mot de passe",
|
||||
"unknown": "Erreur inconnue"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Hôte",
|
||||
"port": "Port",
|
||||
"username": "Nom d'utilisateur",
|
||||
"password": "Mot de passe"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Chaîne"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protocole",
|
||||
"stream": "Flux",
|
||||
"timeout": "Temporisation (en secondes)",
|
||||
"motion_off_delay": "Délai de désactivation du capteur de mouvements (en secondes)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
custom_components/reolink_dev/translations/il.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "כבר מקונפג"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "אין אפשרות להתחבר",
|
||||
"invalid_auth": "הרשאה לא נכונה",
|
||||
"unknown": "בעיה לא ידועה"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "שרת",
|
||||
"port": "פורט",
|
||||
"username": "שם משתמש",
|
||||
"password": "סיסמא"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "ערוץ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "פרוטוקול",
|
||||
"stream": "סטרים",
|
||||
"timeout": "טיימאאוט",
|
||||
"motion_off_delay": "השהיית כבוי תזוזה"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
custom_components/reolink_dev/translations/nl.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Deze camera is al geconfigureerd"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Kan niet verbinden met de camera",
|
||||
"invalid_auth": "Ongeldige gebruikersnaam of wachtwoord",
|
||||
"unknown": "Onbekende fout opgetreden"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Poort",
|
||||
"username": "Gebruikersnaam",
|
||||
"password": "Wachtwoord"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Kanaal"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protocol",
|
||||
"stream": "Stream",
|
||||
"timeout": "Timeout (seconden)",
|
||||
"motion_off_delay": "Bewegingssensor uit vertraging (seconden)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
custom_components/reolink_dev/translations/pl.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Ta kamera jest już skonfigurowana"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Nie udało się połączyć z kamerą",
|
||||
"invalid_auth": "Nieprawidłowy użytkownik lub hasło",
|
||||
"unknown": "Niespodziewany błąd"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"username": "Nazwa użytkownika",
|
||||
"password": "Hasło"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Kanał"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protokół",
|
||||
"stream": "Stream",
|
||||
"timeout": "Timeout (sekundy)",
|
||||
"motion_off_delay": "Opóźnienie wyłączenia czujnika ruchu (sekundy)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
custom_components/reolink_dev/translations/se.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Denna kamera är redan konfiguerad"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Misslyckades med att ansluta till kameran",
|
||||
"invalid_auth": "Fel användarnamn eller lösenord",
|
||||
"unknown": "Oförväntat fel"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"port": "Port",
|
||||
"username": "Användarnamn",
|
||||
"password": "Lösenord"
|
||||
}
|
||||
},
|
||||
"nvr": {
|
||||
"data": {
|
||||
"channel": "Kanal"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"protocol": "Protokoll",
|
||||
"stream": "Ström",
|
||||
"timeout": "Timeout (sekunder)",
|
||||
"motion_off_delay": "Rörelsesensor avstängningfördröjning (sekunder)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
31
custom_components/reolink_dev/typings.py
Normal file
@ -0,0 +1,31 @@
|
||||
""" Typing declarations for strongly typed dictionaries """
|
||||
|
||||
from typing import Any, Dict, List, TypedDict
|
||||
from datetime import datetime, date
|
||||
|
||||
VodEvent = TypedDict(
|
||||
"VodEvent",
|
||||
{
|
||||
"start": datetime,
|
||||
"end": datetime,
|
||||
"file": str,
|
||||
"thumbnail": Any,
|
||||
},
|
||||
total=False,
|
||||
)
|
||||
|
||||
MediaSourceCacheEntry = TypedDict(
|
||||
"MediaSourceCacheEntry",
|
||||
{
|
||||
"entry_id": str,
|
||||
"unique_id": str,
|
||||
"event_id": str,
|
||||
"name": str,
|
||||
"playback_months": int,
|
||||
"playback_thumbnails": bool,
|
||||
"playback_thumbnail_offset": int,
|
||||
"playback_day_entries": List[date],
|
||||
"playback_events": Dict[str, VodEvent],
|
||||
},
|
||||
total=False,
|
||||
)
|
@ -1,28 +1,16 @@
|
||||
# Icons @ https://materialdesignicons.com/
|
||||
# FontAwesome implementation @ https://github.com/thomasloven/hass-fontawesome
|
||||
title: Making Home Great Again
|
||||
|
||||
views:
|
||||
- !include config/views/overview.yaml
|
||||
- !include config/views/floorplan.yaml
|
||||
- !include config/views/lights.yaml
|
||||
- !include config/views/livingroom.yaml
|
||||
- !include config/views/media.yaml
|
||||
- !include config/views/robots.yaml
|
||||
- !include config/views/humidity.yaml
|
||||
- !include config/views/spotify.yaml
|
||||
- !include config/views/floorplan.yaml
|
||||
- !include config/views/instagram.yaml
|
||||
- !include config/views/devices.yaml
|
||||
- !include config/views/landroid.yaml
|
||||
- !include config/views/cctv.yaml
|
||||
- !include config/views/network.yaml
|
||||
- !include config/views/alerts.yaml
|
||||
|
||||
resources:
|
||||
- url: /local/lovelace/resources/weather-card.js
|
||||
type: module
|
||||
- url: /local/lovelace/resources/auto-entities.js
|
||||
type: module
|
||||
- url: /local/lovelace/resources/color-lite-card.js
|
||||
type: js
|
||||
# https://github.com/bradcrc/color-lite-card/
|
||||
- url: /local/lovelace/resources/now-playing-card.js
|
||||
type: js
|
||||
# https://github.com/bradcrc/Now-Playing-Card
|
||||
|
Before Width: | Height: | Size: 34 KiB |
194
www/lovelace/custom/auto-entities/auto-entities.js
Normal file
69
www/lovelace/custom/color-lite-card/color-lite-card.js
Normal file
@ -0,0 +1,69 @@
|
||||
class ColorLite extends HTMLElement {
|
||||
set hass(hass) {
|
||||
if (!this.content) {
|
||||
const card = document.createElement('ha-card');
|
||||
this.content = document.createElement('div');
|
||||
card.appendChild(this.content);
|
||||
card.style.background = 'none';
|
||||
this.appendChild(card);
|
||||
}
|
||||
|
||||
const entityId = this.config.entity;
|
||||
const state = hass.states[entityId];
|
||||
|
||||
|
||||
// if the light is on
|
||||
if(state){
|
||||
if(state.state == 'on'){
|
||||
|
||||
const imageURLId = this.config.image;
|
||||
var ImURL = imageURLId;
|
||||
const imageURLCId = this.config.color_image;
|
||||
var rgbval = state.attributes.rgb_color;
|
||||
var hsval = state.attributes.hs_color;
|
||||
var hsar = "";
|
||||
var min_bright = (this.config.min_brightness * 2.5);
|
||||
var bright = state.attributes.brightness;
|
||||
if (hsval) {
|
||||
if (rgbval != "255,255,255") {
|
||||
var hsar = ' hue-rotate(' + hsval[0] + 'deg)';
|
||||
if (imageURLCId) {
|
||||
ImURL = imageURLCId;
|
||||
}
|
||||
}
|
||||
}
|
||||
var bbritef = bright;
|
||||
if (min_bright > bright) {
|
||||
bbritef = min_bright;
|
||||
}
|
||||
var bbrite = (bbritef / 205);
|
||||
|
||||
this.content.innerHTML = `
|
||||
<!-- Custom Lite Card for x${rgbval}x -->
|
||||
<img src="${ImURL}" style="filter: opacity(${bbrite})${hsar}!important;" width="100%" height="100%">
|
||||
`;
|
||||
|
||||
} else {
|
||||
this.content.innerHTML = `
|
||||
<!-- Custom Lite Card for ${entityId} is turned off -->
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity) {
|
||||
throw new Error('You need to define an entity');
|
||||
}
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
// The height of your card. Home Assistant uses this to automatically
|
||||
// distribute all cards over the available columns.
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('color-lite-card', ColorLite);
|
102
www/lovelace/custom/now-playing-card/now-playing-card.js
Normal file
@ -0,0 +1,102 @@
|
||||
class NowPlayingPoster extends HTMLElement {
|
||||
set hass(hass) {
|
||||
if (!this.content) {
|
||||
const card = document.createElement('ha-card');
|
||||
this.content = document.createElement('div');
|
||||
|
||||
|
||||
//this.content.style = "!important;";
|
||||
|
||||
|
||||
card.appendChild(this.content);
|
||||
card.style = "background: none;";
|
||||
this.appendChild(card);
|
||||
|
||||
|
||||
}
|
||||
|
||||
const offposter = this.config.off_image;
|
||||
const entityId = this.config.entity;
|
||||
const state = hass.states[entityId];
|
||||
const stateStr = state ? state.state : 'unavailable';
|
||||
|
||||
|
||||
|
||||
if (state) {
|
||||
|
||||
const movposter = state.attributes.entity_picture;
|
||||
|
||||
if (["playing", "on"].indexOf(stateStr) > -1 ) {
|
||||
if ( !movposter ) {
|
||||
if ( offposter ) {
|
||||
this.content.innerHTML = `
|
||||
<!-- now playing card ${entityId} -->
|
||||
<img src="${offposter}" width=100% align="center" style="">
|
||||
`;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.content.innerHTML = `
|
||||
<!-- now playing card ${entityId} no image-->
|
||||
`;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.content.innerHTML = `
|
||||
<!-- now playing card ${entityId} -->
|
||||
<img src="${movposter}" width=100% height=100%">
|
||||
`;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if ( offposter ) {
|
||||
this.content.innerHTML = `
|
||||
<!-- now playing card ${entityId} -->
|
||||
<img src="${offposter}" width=100% align="center" style="">
|
||||
`;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.content.innerHTML = `
|
||||
<!-- now playing card ${entityId} no image-->
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
this.content.innerHTML = `
|
||||
<!-- now playing card ${entityId} not playing -->
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
setConfig(config) {
|
||||
if (!config.entity) {
|
||||
throw new Error('You need to define an entity');
|
||||
}
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
|
||||
// The height of your card. Home Assistant uses this to automatically
|
||||
// distribute all cards over the available columns.
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('now-playing-poster', NowPlayingPoster);
|
674
www/lovelace/custom/vacuum-card/vacuum-card.js
Normal file
175
www/lovelace/custom/weather-card/icons/cloudy-day-1.svg
Normal file
@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-sun-shiny {
|
||||
0% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 0.1px 10px;
|
||||
stroke-dashoffset: -1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun-shiny line {
|
||||
-webkit-animation-name: am-weather-sun-shiny;
|
||||
-moz-animation-name: am-weather-sun-shiny;
|
||||
-ms-animation-name: am-weather-sun-shiny;
|
||||
animation-name: am-weather-sun-shiny;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy-day-1">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(0,16)">
|
||||
<g class="am-weather-sun">
|
||||
<g>
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#C6DEFF" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
176
www/lovelace/custom/weather-card/icons/cloudy-day-2.svg
Normal file
@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-sun-shiny {
|
||||
0% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 0.1px 10px;
|
||||
stroke-dashoffset: -1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun-shiny line {
|
||||
-webkit-animation-name: am-weather-sun-shiny;
|
||||
-moz-animation-name: am-weather-sun-shiny;
|
||||
-ms-animation-name: am-weather-sun-shiny;
|
||||
animation-name: am-weather-sun-shiny;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy-day-2">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(0,16)">
|
||||
<g class="am-weather-sun">
|
||||
<g>
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#91C0F8" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
175
www/lovelace/custom/weather-card/icons/cloudy-day-3.svg
Normal file
@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-sun-shiny {
|
||||
0% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 0.1px 10px;
|
||||
stroke-dashoffset: -1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun-shiny line {
|
||||
-webkit-animation-name: am-weather-sun-shiny;
|
||||
-moz-animation-name: am-weather-sun-shiny;
|
||||
-ms-animation-name: am-weather-sun-shiny;
|
||||
animation-name: am-weather-sun-shiny;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy-day-3">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(0,16)">
|
||||
<g class="am-weather-sun">
|
||||
<g>
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
198
www/lovelace/custom/weather-card/icons/cloudy-night-1.svg
Normal file
@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** MOON
|
||||
*/
|
||||
@keyframes am-weather-moon {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
-moz-transform: rotate(15deg);
|
||||
-ms-transform: rotate(15deg);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon {
|
||||
-webkit-animation-name: am-weather-moon;
|
||||
-moz-animation-name: am-weather-moon;
|
||||
-ms-animation-name: am-weather-moon;
|
||||
animation-name: am-weather-moon;
|
||||
-webkit-animation-duration: 6s;
|
||||
-moz-animation-duration: 6s;
|
||||
-ms-animation-duration: 6s;
|
||||
animation-duration: 6s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-1 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-1 {
|
||||
-webkit-animation-name: am-weather-moon-star-1;
|
||||
-moz-animation-name: am-weather-moon-star-1;
|
||||
-ms-animation-name: am-weather-moon-star-1;
|
||||
animation-name: am-weather-moon-star-1;
|
||||
-webkit-animation-delay: 3s;
|
||||
-moz-animation-delay: 3s;
|
||||
-ms-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
-ms-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-2 {
|
||||
-webkit-animation-name: am-weather-moon-star-2;
|
||||
-moz-animation-name: am-weather-moon-star-2;
|
||||
-ms-animation-name: am-weather-moon-star-2;
|
||||
animation-name: am-weather-moon-star-2;
|
||||
-webkit-animation-delay: 5s;
|
||||
-moz-animation-delay: 5s;
|
||||
-ms-animation-delay: 5s;
|
||||
animation-delay: 5s;
|
||||
-webkit-animation-duration: 4s;
|
||||
-moz-animation-duration: 4s;
|
||||
-ms-animation-duration: 4s;
|
||||
animation-duration: 4s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy-night-1">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(16,4), scale(0.8)">
|
||||
<g class="am-weather-moon-star-1">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<g class="am-weather-moon-star-2">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
|
||||
</g>
|
||||
<g class="am-weather-moon">
|
||||
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#C6DEFF" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
198
www/lovelace/custom/weather-card/icons/cloudy-night-2.svg
Normal file
@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** MOON
|
||||
*/
|
||||
@keyframes am-weather-moon {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
-moz-transform: rotate(15deg);
|
||||
-ms-transform: rotate(15deg);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon {
|
||||
-webkit-animation-name: am-weather-moon;
|
||||
-moz-animation-name: am-weather-moon;
|
||||
-ms-animation-name: am-weather-moon;
|
||||
animation-name: am-weather-moon;
|
||||
-webkit-animation-duration: 6s;
|
||||
-moz-animation-duration: 6s;
|
||||
-ms-animation-duration: 6s;
|
||||
animation-duration: 6s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-1 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-1 {
|
||||
-webkit-animation-name: am-weather-moon-star-1;
|
||||
-moz-animation-name: am-weather-moon-star-1;
|
||||
-ms-animation-name: am-weather-moon-star-1;
|
||||
animation-name: am-weather-moon-star-1;
|
||||
-webkit-animation-delay: 3s;
|
||||
-moz-animation-delay: 3s;
|
||||
-ms-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
-ms-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-2 {
|
||||
-webkit-animation-name: am-weather-moon-star-2;
|
||||
-moz-animation-name: am-weather-moon-star-2;
|
||||
-ms-animation-name: am-weather-moon-star-2;
|
||||
animation-name: am-weather-moon-star-2;
|
||||
-webkit-animation-delay: 5s;
|
||||
-moz-animation-delay: 5s;
|
||||
-ms-animation-delay: 5s;
|
||||
animation-delay: 5s;
|
||||
-webkit-animation-duration: 4s;
|
||||
-moz-animation-duration: 4s;
|
||||
-ms-animation-duration: 4s;
|
||||
animation-duration: 4s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy-night-2">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(16,4), scale(0.8)">
|
||||
<g class="am-weather-moon-star-1">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<g class="am-weather-moon-star-2">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
|
||||
</g>
|
||||
<g class="am-weather-moon">
|
||||
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#91C0F8" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
198
www/lovelace/custom/weather-card/icons/cloudy-night-3.svg
Normal file
@ -0,0 +1,198 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** MOON
|
||||
*/
|
||||
@keyframes am-weather-moon {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
-moz-transform: rotate(15deg);
|
||||
-ms-transform: rotate(15deg);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon {
|
||||
-webkit-animation-name: am-weather-moon;
|
||||
-moz-animation-name: am-weather-moon;
|
||||
-ms-animation-name: am-weather-moon;
|
||||
animation-name: am-weather-moon;
|
||||
-webkit-animation-duration: 6s;
|
||||
-moz-animation-duration: 6s;
|
||||
-ms-animation-duration: 6s;
|
||||
animation-duration: 6s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-1 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-1 {
|
||||
-webkit-animation-name: am-weather-moon-star-1;
|
||||
-moz-animation-name: am-weather-moon-star-1;
|
||||
-ms-animation-name: am-weather-moon-star-1;
|
||||
animation-name: am-weather-moon-star-1;
|
||||
-webkit-animation-delay: 3s;
|
||||
-moz-animation-delay: 3s;
|
||||
-ms-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
-ms-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-2 {
|
||||
-webkit-animation-name: am-weather-moon-star-2;
|
||||
-moz-animation-name: am-weather-moon-star-2;
|
||||
-ms-animation-name: am-weather-moon-star-2;
|
||||
animation-name: am-weather-moon-star-2;
|
||||
-webkit-animation-delay: 5s;
|
||||
-moz-animation-delay: 5s;
|
||||
-ms-animation-delay: 5s;
|
||||
animation-delay: 5s;
|
||||
-webkit-animation-duration: 4s;
|
||||
-moz-animation-duration: 4s;
|
||||
-ms-animation-duration: 4s;
|
||||
animation-duration: 4s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy-night-3">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(16,4), scale(0.8)">
|
||||
<g class="am-weather-moon-star-1">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<g class="am-weather-moon-star-2">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
|
||||
</g>
|
||||
<g class="am-weather-moon">
|
||||
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
500
www/lovelace/custom/weather-card/icons/cloudy.svg
Normal file
@ -0,0 +1,500 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-1 {
|
||||
0% {
|
||||
-webkit-transform: translate(-5px,0px);
|
||||
-moz-transform: translate(-5px,0px);
|
||||
-ms-transform: translate(-5px,0px);
|
||||
transform: translate(-5px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(10px,0px);
|
||||
-moz-transform: translate(10px,0px);
|
||||
-ms-transform: translate(10px,0px);
|
||||
transform: translate(10px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(-5px,0px);
|
||||
-moz-transform: translate(-5px,0px);
|
||||
-ms-transform: translate(-5px,0px);
|
||||
transform: translate(-5px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-1 {
|
||||
-webkit-animation-name: am-weather-cloud-1;
|
||||
-moz-animation-name: am-weather-cloud-1;
|
||||
animation-name: am-weather-cloud-1;
|
||||
-webkit-animation-duration: 7s;
|
||||
-moz-animation-duration: 7s;
|
||||
animation-duration: 7s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-sun-shiny {
|
||||
0% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 0.1px 10px;
|
||||
stroke-dashoffset: -1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun-shiny line {
|
||||
-webkit-animation-name: am-weather-sun-shiny;
|
||||
-moz-animation-name: am-weather-sun-shiny;
|
||||
-ms-animation-name: am-weather-sun-shiny;
|
||||
animation-name: am-weather-sun-shiny;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
** MOON
|
||||
*/
|
||||
@keyframes am-weather-moon {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
-moz-transform: rotate(15deg);
|
||||
-ms-transform: rotate(15deg);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon {
|
||||
-webkit-animation-name: am-weather-moon;
|
||||
-moz-animation-name: am-weather-moon;
|
||||
-ms-animation-name: am-weather-moon;
|
||||
animation-name: am-weather-moon;
|
||||
-webkit-animation-duration: 6s;
|
||||
-moz-animation-duration: 6s;
|
||||
-ms-animation-duration: 6s;
|
||||
animation-duration: 6s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-1 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-1 {
|
||||
-webkit-animation-name: am-weather-moon-star-1;
|
||||
-moz-animation-name: am-weather-moon-star-1;
|
||||
-ms-animation-name: am-weather-moon-star-1;
|
||||
animation-name: am-weather-moon-star-1;
|
||||
-webkit-animation-delay: 3s;
|
||||
-moz-animation-delay: 3s;
|
||||
-ms-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
-ms-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-2 {
|
||||
-webkit-animation-name: am-weather-moon-star-2;
|
||||
-moz-animation-name: am-weather-moon-star-2;
|
||||
-ms-animation-name: am-weather-moon-star-2;
|
||||
animation-name: am-weather-moon-star-2;
|
||||
-webkit-animation-delay: 5s;
|
||||
-moz-animation-delay: 5s;
|
||||
-ms-animation-delay: 5s;
|
||||
animation-delay: 5s;
|
||||
-webkit-animation-duration: 4s;
|
||||
-moz-animation-duration: 4s;
|
||||
-ms-animation-duration: 4s;
|
||||
animation-duration: 4s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
** RAIN
|
||||
*/
|
||||
@keyframes am-weather-rain {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-rain-1 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-rain-2 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-delay: 0.25s;
|
||||
-moz-animation-delay: 0.25s;
|
||||
-ms-animation-delay: 0.25s;
|
||||
animation-delay: 0.25s;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** SNOW
|
||||
*/
|
||||
@keyframes am-weather-snow {
|
||||
0% {
|
||||
-webkit-transform: translateX(0) translateY(0);
|
||||
-moz-transform: translateX(0) translateY(0);
|
||||
-ms-transform: translateX(0) translateY(0);
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33.33% {
|
||||
-webkit-transform: translateX(-1.2px) translateY(2px);
|
||||
-moz-transform: translateX(-1.2px) translateY(2px);
|
||||
-ms-transform: translateX(-1.2px) translateY(2px);
|
||||
transform: translateX(-1.2px) translateY(2px);
|
||||
}
|
||||
|
||||
66.66% {
|
||||
-webkit-transform: translateX(1.4px) translateY(4px);
|
||||
-moz-transform: translateX(1.4px) translateY(4px);
|
||||
-ms-transform: translateX(1.4px) translateY(4px);
|
||||
transform: translateX(1.4px) translateY(4px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateX(-1.6px) translateY(6px);
|
||||
-moz-transform: translateX(-1.6px) translateY(6px);
|
||||
-ms-transform: translateX(-1.6px) translateY(6px);
|
||||
transform: translateX(-1.6px) translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes am-weather-snow-reverse {
|
||||
0% {
|
||||
-webkit-transform: translateX(0) translateY(0);
|
||||
-moz-transform: translateX(0) translateY(0);
|
||||
-ms-transform: translateX(0) translateY(0);
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33.33% {
|
||||
-webkit-transform: translateX(1.2px) translateY(2px);
|
||||
-moz-transform: translateX(1.2px) translateY(2px);
|
||||
-ms-transform: translateX(1.2px) translateY(2px);
|
||||
transform: translateX(1.2px) translateY(2px);
|
||||
}
|
||||
|
||||
66.66% {
|
||||
-webkit-transform: translateX(-1.4px) translateY(4px);
|
||||
-moz-transform: translateX(-1.4px) translateY(4px);
|
||||
-ms-transform: translateX(-1.4px) translateY(4px);
|
||||
transform: translateX(-1.4px) translateY(4px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateX(1.6px) translateY(6px);
|
||||
-moz-transform: translateX(1.6px) translateY(6px);
|
||||
-ms-transform: translateX(1.6px) translateY(6px);
|
||||
transform: translateX(1.6px) translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-snow-1 {
|
||||
-webkit-animation-name: am-weather-snow;
|
||||
-moz-animation-name: am-weather-snow;
|
||||
-ms-animation-name: am-weather-snow;
|
||||
animation-name: am-weather-snow;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-snow-2 {
|
||||
-webkit-animation-name: am-weather-snow;
|
||||
-moz-animation-name: am-weather-snow;
|
||||
-ms-animation-name: am-weather-snow;
|
||||
animation-name: am-weather-snow;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
-moz-animation-delay: 1.2s;
|
||||
-ms-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-snow-3 {
|
||||
-webkit-animation-name: am-weather-snow-reverse;
|
||||
-moz-animation-name: am-weather-snow-reverse;
|
||||
-ms-animation-name: am-weather-snow-reverse;
|
||||
animation-name: am-weather-snow-reverse;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** EASING
|
||||
*/
|
||||
.am-weather-easing-ease-in-out {
|
||||
-webkit-animation-timing-function: ease-in-out;
|
||||
-moz-animation-timing-function: ease-in-out;
|
||||
-ms-animation-timing-function: ease-in-out;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="cloudy">
|
||||
<g transform="translate(20,10)">
|
||||
<g class="am-weather-cloud-1">
|
||||
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#91C0F8" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-10,-8), scale(0.6)"/>
|
||||
</g>
|
||||
<g class="am-weather-cloud-2">
|
||||
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 15 KiB |
521
www/lovelace/custom/weather-card/icons/day.svg
Normal file
@ -0,0 +1,521 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-1 {
|
||||
0% {
|
||||
-webkit-transform: translate(-5px,0px);
|
||||
-moz-transform: translate(-5px,0px);
|
||||
-ms-transform: translate(-5px,0px);
|
||||
transform: translate(-5px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(10px,0px);
|
||||
-moz-transform: translate(10px,0px);
|
||||
-ms-transform: translate(10px,0px);
|
||||
transform: translate(10px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(-5px,0px);
|
||||
-moz-transform: translate(-5px,0px);
|
||||
-ms-transform: translate(-5px,0px);
|
||||
transform: translate(-5px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-1 {
|
||||
-webkit-animation-name: am-weather-cloud-1;
|
||||
-moz-animation-name: am-weather-cloud-1;
|
||||
animation-name: am-weather-cloud-1;
|
||||
-webkit-animation-duration: 7s;
|
||||
-moz-animation-duration: 7s;
|
||||
animation-duration: 7s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-sun-shiny {
|
||||
0% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 0.1px 10px;
|
||||
stroke-dashoffset: -1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun-shiny line {
|
||||
-webkit-animation-name: am-weather-sun-shiny;
|
||||
-moz-animation-name: am-weather-sun-shiny;
|
||||
-ms-animation-name: am-weather-sun-shiny;
|
||||
animation-name: am-weather-sun-shiny;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
** MOON
|
||||
*/
|
||||
@keyframes am-weather-moon {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
-moz-transform: rotate(15deg);
|
||||
-ms-transform: rotate(15deg);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon {
|
||||
-webkit-animation-name: am-weather-moon;
|
||||
-moz-animation-name: am-weather-moon;
|
||||
-ms-animation-name: am-weather-moon;
|
||||
animation-name: am-weather-moon;
|
||||
-webkit-animation-duration: 6s;
|
||||
-moz-animation-duration: 6s;
|
||||
-ms-animation-duration: 6s;
|
||||
animation-duration: 6s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-1 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-1 {
|
||||
-webkit-animation-name: am-weather-moon-star-1;
|
||||
-moz-animation-name: am-weather-moon-star-1;
|
||||
-ms-animation-name: am-weather-moon-star-1;
|
||||
animation-name: am-weather-moon-star-1;
|
||||
-webkit-animation-delay: 3s;
|
||||
-moz-animation-delay: 3s;
|
||||
-ms-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
-ms-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-2 {
|
||||
-webkit-animation-name: am-weather-moon-star-2;
|
||||
-moz-animation-name: am-weather-moon-star-2;
|
||||
-ms-animation-name: am-weather-moon-star-2;
|
||||
animation-name: am-weather-moon-star-2;
|
||||
-webkit-animation-delay: 5s;
|
||||
-moz-animation-delay: 5s;
|
||||
-ms-animation-delay: 5s;
|
||||
animation-delay: 5s;
|
||||
-webkit-animation-duration: 4s;
|
||||
-moz-animation-duration: 4s;
|
||||
-ms-animation-duration: 4s;
|
||||
animation-duration: 4s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
** RAIN
|
||||
*/
|
||||
@keyframes am-weather-rain {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-rain-1 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-rain-2 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-delay: 0.25s;
|
||||
-moz-animation-delay: 0.25s;
|
||||
-ms-animation-delay: 0.25s;
|
||||
animation-delay: 0.25s;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** SNOW
|
||||
*/
|
||||
@keyframes am-weather-snow {
|
||||
0% {
|
||||
-webkit-transform: translateX(0) translateY(0);
|
||||
-moz-transform: translateX(0) translateY(0);
|
||||
-ms-transform: translateX(0) translateY(0);
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33.33% {
|
||||
-webkit-transform: translateX(-1.2px) translateY(2px);
|
||||
-moz-transform: translateX(-1.2px) translateY(2px);
|
||||
-ms-transform: translateX(-1.2px) translateY(2px);
|
||||
transform: translateX(-1.2px) translateY(2px);
|
||||
}
|
||||
|
||||
66.66% {
|
||||
-webkit-transform: translateX(1.4px) translateY(4px);
|
||||
-moz-transform: translateX(1.4px) translateY(4px);
|
||||
-ms-transform: translateX(1.4px) translateY(4px);
|
||||
transform: translateX(1.4px) translateY(4px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateX(-1.6px) translateY(6px);
|
||||
-moz-transform: translateX(-1.6px) translateY(6px);
|
||||
-ms-transform: translateX(-1.6px) translateY(6px);
|
||||
transform: translateX(-1.6px) translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes am-weather-snow-reverse {
|
||||
0% {
|
||||
-webkit-transform: translateX(0) translateY(0);
|
||||
-moz-transform: translateX(0) translateY(0);
|
||||
-ms-transform: translateX(0) translateY(0);
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33.33% {
|
||||
-webkit-transform: translateX(1.2px) translateY(2px);
|
||||
-moz-transform: translateX(1.2px) translateY(2px);
|
||||
-ms-transform: translateX(1.2px) translateY(2px);
|
||||
transform: translateX(1.2px) translateY(2px);
|
||||
}
|
||||
|
||||
66.66% {
|
||||
-webkit-transform: translateX(-1.4px) translateY(4px);
|
||||
-moz-transform: translateX(-1.4px) translateY(4px);
|
||||
-ms-transform: translateX(-1.4px) translateY(4px);
|
||||
transform: translateX(-1.4px) translateY(4px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateX(1.6px) translateY(6px);
|
||||
-moz-transform: translateX(1.6px) translateY(6px);
|
||||
-ms-transform: translateX(1.6px) translateY(6px);
|
||||
transform: translateX(1.6px) translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-snow-1 {
|
||||
-webkit-animation-name: am-weather-snow;
|
||||
-moz-animation-name: am-weather-snow;
|
||||
-ms-animation-name: am-weather-snow;
|
||||
animation-name: am-weather-snow;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-snow-2 {
|
||||
-webkit-animation-name: am-weather-snow;
|
||||
-moz-animation-name: am-weather-snow;
|
||||
-ms-animation-name: am-weather-snow;
|
||||
animation-name: am-weather-snow;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
-moz-animation-delay: 1.2s;
|
||||
-ms-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-snow-3 {
|
||||
-webkit-animation-name: am-weather-snow-reverse;
|
||||
-moz-animation-name: am-weather-snow-reverse;
|
||||
-ms-animation-name: am-weather-snow-reverse;
|
||||
animation-name: am-weather-snow-reverse;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** EASING
|
||||
*/
|
||||
.am-weather-easing-ease-in-out {
|
||||
-webkit-animation-timing-function: ease-in-out;
|
||||
-moz-animation-timing-function: ease-in-out;
|
||||
-ms-animation-timing-function: ease-in-out;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="day">
|
||||
<g transform="translate(32,32)">
|
||||
<g class="am-weather-sun am-weather-sun-shiny am-weather-easing-ease-in-out">
|
||||
<g>
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 16 KiB |
503
www/lovelace/custom/weather-card/icons/night.svg
Normal file
@ -0,0 +1,503 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** CLOUDS
|
||||
*/
|
||||
@keyframes am-weather-cloud-1 {
|
||||
0% {
|
||||
-webkit-transform: translate(-5px,0px);
|
||||
-moz-transform: translate(-5px,0px);
|
||||
-ms-transform: translate(-5px,0px);
|
||||
transform: translate(-5px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(10px,0px);
|
||||
-moz-transform: translate(10px,0px);
|
||||
-ms-transform: translate(10px,0px);
|
||||
transform: translate(10px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(-5px,0px);
|
||||
-moz-transform: translate(-5px,0px);
|
||||
-ms-transform: translate(-5px,0px);
|
||||
transform: translate(-5px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-1 {
|
||||
-webkit-animation-name: am-weather-cloud-1;
|
||||
-moz-animation-name: am-weather-cloud-1;
|
||||
animation-name: am-weather-cloud-1;
|
||||
-webkit-animation-duration: 7s;
|
||||
-moz-animation-duration: 7s;
|
||||
animation-duration: 7s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-cloud-2 {
|
||||
0% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: translate(2px,0px);
|
||||
-moz-transform: translate(2px,0px);
|
||||
-ms-transform: translate(2px,0px);
|
||||
transform: translate(2px,0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translate(0px,0px);
|
||||
-moz-transform: translate(0px,0px);
|
||||
-ms-transform: translate(0px,0px);
|
||||
transform: translate(0px,0px);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-cloud-2 {
|
||||
-webkit-animation-name: am-weather-cloud-2;
|
||||
-moz-animation-name: am-weather-cloud-2;
|
||||
animation-name: am-weather-cloud-2;
|
||||
-webkit-animation-duration: 3s;
|
||||
-moz-animation-duration: 3s;
|
||||
animation-duration: 3s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes am-weather-sun-shiny {
|
||||
0% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
|
||||
50% {
|
||||
stroke-dasharray: 0.1px 10px;
|
||||
stroke-dashoffset: -1px;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dasharray: 3px 10px;
|
||||
stroke-dashoffset: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun-shiny line {
|
||||
-webkit-animation-name: am-weather-sun-shiny;
|
||||
-moz-animation-name: am-weather-sun-shiny;
|
||||
-ms-animation-name: am-weather-sun-shiny;
|
||||
animation-name: am-weather-sun-shiny;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
** MOON
|
||||
*/
|
||||
@keyframes am-weather-moon {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: rotate(15deg);
|
||||
-moz-transform: rotate(15deg);
|
||||
-ms-transform: rotate(15deg);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon {
|
||||
-webkit-animation-name: am-weather-moon;
|
||||
-moz-animation-name: am-weather-moon;
|
||||
-ms-animation-name: am-weather-moon;
|
||||
animation-name: am-weather-moon;
|
||||
-webkit-animation-duration: 6s;
|
||||
-moz-animation-duration: 6s;
|
||||
-ms-animation-duration: 6s;
|
||||
animation-duration: 6s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-1 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-1 {
|
||||
-webkit-animation-name: am-weather-moon-star-1;
|
||||
-moz-animation-name: am-weather-moon-star-1;
|
||||
-ms-animation-name: am-weather-moon-star-1;
|
||||
animation-name: am-weather-moon-star-1;
|
||||
-webkit-animation-delay: 3s;
|
||||
-moz-animation-delay: 3s;
|
||||
-ms-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
-webkit-animation-duration: 5s;
|
||||
-moz-animation-duration: 5s;
|
||||
-ms-animation-duration: 5s;
|
||||
animation-duration: 5s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes am-weather-moon-star-2 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-moon-star-2 {
|
||||
-webkit-animation-name: am-weather-moon-star-2;
|
||||
-moz-animation-name: am-weather-moon-star-2;
|
||||
-ms-animation-name: am-weather-moon-star-2;
|
||||
animation-name: am-weather-moon-star-2;
|
||||
-webkit-animation-delay: 5s;
|
||||
-moz-animation-delay: 5s;
|
||||
-ms-animation-delay: 5s;
|
||||
animation-delay: 5s;
|
||||
-webkit-animation-duration: 4s;
|
||||
-moz-animation-duration: 4s;
|
||||
-ms-animation-duration: 4s;
|
||||
animation-duration: 4s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: 1;
|
||||
-moz-animation-iteration-count: 1;
|
||||
-ms-animation-iteration-count: 1;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
** RAIN
|
||||
*/
|
||||
@keyframes am-weather-rain {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-rain-1 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-rain-2 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-delay: 0.25s;
|
||||
-moz-animation-delay: 0.25s;
|
||||
-ms-animation-delay: 0.25s;
|
||||
animation-delay: 0.25s;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
** SNOW
|
||||
*/
|
||||
@keyframes am-weather-snow {
|
||||
0% {
|
||||
-webkit-transform: translateX(0) translateY(0);
|
||||
-moz-transform: translateX(0) translateY(0);
|
||||
-ms-transform: translateX(0) translateY(0);
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33.33% {
|
||||
-webkit-transform: translateX(-1.2px) translateY(2px);
|
||||
-moz-transform: translateX(-1.2px) translateY(2px);
|
||||
-ms-transform: translateX(-1.2px) translateY(2px);
|
||||
transform: translateX(-1.2px) translateY(2px);
|
||||
}
|
||||
|
||||
66.66% {
|
||||
-webkit-transform: translateX(1.4px) translateY(4px);
|
||||
-moz-transform: translateX(1.4px) translateY(4px);
|
||||
-ms-transform: translateX(1.4px) translateY(4px);
|
||||
transform: translateX(1.4px) translateY(4px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateX(-1.6px) translateY(6px);
|
||||
-moz-transform: translateX(-1.6px) translateY(6px);
|
||||
-ms-transform: translateX(-1.6px) translateY(6px);
|
||||
transform: translateX(-1.6px) translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes am-weather-snow-reverse {
|
||||
0% {
|
||||
-webkit-transform: translateX(0) translateY(0);
|
||||
-moz-transform: translateX(0) translateY(0);
|
||||
-ms-transform: translateX(0) translateY(0);
|
||||
transform: translateX(0) translateY(0);
|
||||
}
|
||||
|
||||
33.33% {
|
||||
-webkit-transform: translateX(1.2px) translateY(2px);
|
||||
-moz-transform: translateX(1.2px) translateY(2px);
|
||||
-ms-transform: translateX(1.2px) translateY(2px);
|
||||
transform: translateX(1.2px) translateY(2px);
|
||||
}
|
||||
|
||||
66.66% {
|
||||
-webkit-transform: translateX(-1.4px) translateY(4px);
|
||||
-moz-transform: translateX(-1.4px) translateY(4px);
|
||||
-ms-transform: translateX(-1.4px) translateY(4px);
|
||||
transform: translateX(-1.4px) translateY(4px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: translateX(1.6px) translateY(6px);
|
||||
-moz-transform: translateX(1.6px) translateY(6px);
|
||||
-ms-transform: translateX(1.6px) translateY(6px);
|
||||
transform: translateX(1.6px) translateY(6px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-snow-1 {
|
||||
-webkit-animation-name: am-weather-snow;
|
||||
-moz-animation-name: am-weather-snow;
|
||||
-ms-animation-name: am-weather-snow;
|
||||
animation-name: am-weather-snow;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-snow-2 {
|
||||
-webkit-animation-name: am-weather-snow;
|
||||
-moz-animation-name: am-weather-snow;
|
||||
-ms-animation-name: am-weather-snow;
|
||||
animation-name: am-weather-snow;
|
||||
-webkit-animation-delay: 1.2s;
|
||||
-moz-animation-delay: 1.2s;
|
||||
-ms-animation-delay: 1.2s;
|
||||
animation-delay: 1.2s;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-snow-3 {
|
||||
-webkit-animation-name: am-weather-snow-reverse;
|
||||
-moz-animation-name: am-weather-snow-reverse;
|
||||
-ms-animation-name: am-weather-snow-reverse;
|
||||
animation-name: am-weather-snow-reverse;
|
||||
-webkit-animation-duration: 2s;
|
||||
-moz-animation-duration: 2s;
|
||||
-ms-animation-duration: 2s;
|
||||
animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** EASING
|
||||
*/
|
||||
.am-weather-easing-ease-in-out {
|
||||
-webkit-animation-timing-function: ease-in-out;
|
||||
-moz-animation-timing-function: ease-in-out;
|
||||
-ms-animation-timing-function: ease-in-out;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="night">
|
||||
<g transform="translate(20,20)">
|
||||
<g class="am-weather-moon-star-1">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<g class="am-weather-moon-star-2">
|
||||
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
|
||||
</g>
|
||||
<g class="am-weather-moon">
|
||||
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 15 KiB |
157
www/lovelace/custom/weather-card/icons/rainy-1.svg
Normal file
@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** RAIN
|
||||
*/
|
||||
@keyframes am-weather-rain {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-rain-1 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-rain-2 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-delay: 0.25s;
|
||||
-moz-animation-delay: 0.25s;
|
||||
-ms-animation-delay: 0.25s;
|
||||
animation-delay: 0.25s;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="rainy-1">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(0,16), scale(1.2)">
|
||||
<g class="am-weather-sun">
|
||||
<g>
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.5" transform="translate(-15,-5), scale(0.85)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(34,46), rotate(10)">
|
||||
<line class="am-weather-rain-1" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(-6,1)" x1="0" x2="0" y1="0" y2="8" />
|
||||
<line class="am-weather-rain-2" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(0,-1)" x1="0" x2="0" y1="0" y2="8" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
133
www/lovelace/custom/weather-card/icons/rainy-2.svg
Normal file
@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** RAIN
|
||||
*/
|
||||
@keyframes am-weather-rain {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-rain-1 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="rainy-2">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(0,16)">
|
||||
<g class="am-weather-sun">
|
||||
<g>
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(37,45), rotate(10)">
|
||||
<line class="am-weather-rain-1" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(-6,1)" x1="0" x2="0" y1="0" y2="8" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.3 KiB |
157
www/lovelace/custom/weather-card/icons/rainy-3.svg
Normal file
@ -0,0 +1,157 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- (c) ammap.com | SVG weather icons -->
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="64"
|
||||
height="64"
|
||||
viewbox="0 0 64 64">
|
||||
<defs>
|
||||
<filter id="blur" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
|
||||
<feOffset dx="0" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.05"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<style type="text/css"><![CDATA[
|
||||
/*
|
||||
** SUN
|
||||
*/
|
||||
@keyframes am-weather-sun {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
-moz-transform: rotate(0deg);
|
||||
-ms-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
-moz-transform: rotate(360deg);
|
||||
-ms-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-sun {
|
||||
-webkit-animation-name: am-weather-sun;
|
||||
-moz-animation-name: am-weather-sun;
|
||||
-ms-animation-name: am-weather-sun;
|
||||
animation-name: am-weather-sun;
|
||||
-webkit-animation-duration: 9s;
|
||||
-moz-animation-duration: 9s;
|
||||
-ms-animation-duration: 9s;
|
||||
animation-duration: 9s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
/*
|
||||
** RAIN
|
||||
*/
|
||||
@keyframes am-weather-rain {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
|
||||
.am-weather-rain-1 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.am-weather-rain-2 {
|
||||
-webkit-animation-name: am-weather-rain;
|
||||
-moz-animation-name: am-weather-rain;
|
||||
-ms-animation-name: am-weather-rain;
|
||||
animation-name: am-weather-rain;
|
||||
-webkit-animation-delay: 0.25s;
|
||||
-moz-animation-delay: 0.25s;
|
||||
-ms-animation-delay: 0.25s;
|
||||
animation-delay: 0.25s;
|
||||
-webkit-animation-duration: 8s;
|
||||
-moz-animation-duration: 8s;
|
||||
-ms-animation-duration: 8s;
|
||||
animation-duration: 8s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-moz-animation-timing-function: linear;
|
||||
-ms-animation-timing-function: linear;
|
||||
animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
-ms-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g filter="url(#blur)" id="rainy-3">
|
||||
<g transform="translate(20,10)">
|
||||
<g transform="translate(0,16)">
|
||||
<g class="am-weather-sun">
|
||||
<g>
|
||||
<line fifll="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(45)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(90)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(135)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(180)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(225)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(270)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
<g transform="rotate(315)">
|
||||
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
|
||||
</g>
|
||||
</g>
|
||||
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(34,46), rotate(10)">
|
||||
<line class="am-weather-rain-1" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(-6,1)" x1="0" x2="0" y1="0" y2="8" />
|
||||
<line class="am-weather-rain-2" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(0,-1)" x1="0" x2="0" y1="0" y2="8" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.3 KiB |