Update HA, add vacuum, grid cards, fixes
|
@ -2,6 +2,8 @@
|
||||||
/*
|
/*
|
||||||
|
|
||||||
# Allow
|
# Allow
|
||||||
|
!/.vscode/
|
||||||
|
|
||||||
!*.yaml
|
!*.yaml
|
||||||
!*.jpg
|
!*.jpg
|
||||||
!*.png
|
!*.png
|
||||||
|
@ -23,6 +25,7 @@ android/
|
||||||
ssh-key/
|
ssh-key/
|
||||||
|
|
||||||
ip_bans.yaml
|
ip_bans.yaml
|
||||||
|
google_assistant_service_keys.json
|
||||||
secrets.yaml
|
secrets.yaml
|
||||||
secrets.js
|
secrets.js
|
||||||
known_devices.yaml
|
known_devices.yaml
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"files.associations": {
|
||||||
|
"*.yaml": "home-assistant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
### Security
|
||||||
* [Yi Home Camera 1080p](https://amzn.to/2SYhoW6) - with [custom firmware and mqtt add-on](https://github.com/fbrinker/yi-hack-mqtt)
|
* [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.
|
Work in progress. Not all of them are integrated yet.
|
|
@ -30,13 +30,13 @@ garage_door_open:
|
||||||
name: Garagentor geöffnet
|
name: Garagentor geöffnet
|
||||||
message: "Zur Info - Die *Garage* steht noch *offen*!"
|
message: "Zur Info - Die *Garage* steht noch *offen*!"
|
||||||
done_message: "Zur Info - Die *Garage* ist wieder *geschlossen*."
|
done_message: "Zur Info - Die *Garage* ist wieder *geschlossen*."
|
||||||
entity_id: binary_sensor.lumi_garage_door
|
entity_id: binary_sensor.garage_door
|
||||||
state: "on"
|
state: "on"
|
||||||
repeat:
|
repeat:
|
||||||
- 10
|
- 10
|
||||||
- 30
|
- 30
|
||||||
can_acknowledge: true
|
can_acknowledge: true
|
||||||
skip_first: true
|
skip_first: false
|
||||||
notifiers:
|
notifiers:
|
||||||
- telegram_group
|
- telegram_group
|
||||||
- alexa_all
|
- alexa_all
|
|
@ -1,6 +1,7 @@
|
||||||
# Display Categories: https://developer.amazon.com/de/docs/device-apis/alexa-discovery.html#display-categories
|
# Display Categories: https://developer.amazon.com/de/docs/device-apis/alexa-discovery.html#display-categories
|
||||||
smart_home:
|
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_id: !secret alexa_client_id
|
||||||
client_secret: !secret alexa_client_secret
|
client_secret: !secret alexa_client_secret
|
||||||
filter:
|
filter:
|
||||||
|
@ -13,6 +14,7 @@
|
||||||
- light.onair
|
- light.onair
|
||||||
- light.stimmungslicht
|
- light.stimmungslicht
|
||||||
- light.lichterkette
|
- light.lichterkette
|
||||||
|
- light.philips_iris
|
||||||
- switch.livingroom_music
|
- switch.livingroom_music
|
||||||
- switch.livingroom_netflix
|
- switch.livingroom_netflix
|
||||||
- switch.harmony_firetv
|
- switch.harmony_firetv
|
||||||
|
@ -20,6 +22,7 @@
|
||||||
- switch.harmony_playstation
|
- switch.harmony_playstation
|
||||||
- switch.harmony_denon_power
|
- switch.harmony_denon_power
|
||||||
- switch.desktop_wol
|
- switch.desktop_wol
|
||||||
|
- switch.desktop_jenny_wol
|
||||||
- switch.wallboard_display
|
- switch.wallboard_display
|
||||||
- switch.tplink1
|
- switch.tplink1
|
||||||
- switch.osram_plug_01_57b6060a_on_off
|
- switch.osram_plug_01_57b6060a_on_off
|
||||||
|
@ -43,7 +46,7 @@
|
||||||
name: Esstisch
|
name: Esstisch
|
||||||
description: Esstisch-Lichter
|
description: Esstisch-Lichter
|
||||||
light.office_rgb:
|
light.office_rgb:
|
||||||
name: Büro
|
name: Bürolicht
|
||||||
description: Büro - Deckenlampe
|
description: Büro - Deckenlampe
|
||||||
light.lichtleiste:
|
light.lichtleiste:
|
||||||
name: Schreibtischlicht
|
name: Schreibtischlicht
|
||||||
|
@ -57,6 +60,9 @@
|
||||||
light.onair:
|
light.onair:
|
||||||
name: Studio-Treppe
|
name: Studio-Treppe
|
||||||
description: Studio-Treppenlicht
|
description: Studio-Treppenlicht
|
||||||
|
light.philips_iris:
|
||||||
|
name: Iris
|
||||||
|
description: Studio-Stimmungslicht
|
||||||
switch.livingroom_music:
|
switch.livingroom_music:
|
||||||
name: Musik
|
name: Musik
|
||||||
description: Wohnzimmer - Musik
|
description: Wohnzimmer - Musik
|
||||||
|
@ -78,6 +84,9 @@
|
||||||
switch.desktop_wol:
|
switch.desktop_wol:
|
||||||
name: Computer
|
name: Computer
|
||||||
description: Computer im Büro
|
description: Computer im Büro
|
||||||
|
switch.desktop_jenny_wol:
|
||||||
|
name: Ronny
|
||||||
|
description: Jennys Computer
|
||||||
switch.wallboard_display:
|
switch.wallboard_display:
|
||||||
name: Display
|
name: Display
|
||||||
description: Wallboard 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
|
- alias: Ambilight HDMI
|
||||||
trigger:
|
trigger:
|
||||||
platform: state
|
platform: state
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
- alias: Wallboard On
|
#- alias: Wallboard On
|
||||||
trigger:
|
# trigger:
|
||||||
- platform: state
|
# - platform: state
|
||||||
entity_id: binary_sensor.anyone_home
|
# entity_id: binary_sensor.anyone_home
|
||||||
to: 'on'
|
# to: 'on'
|
||||||
- platform: state
|
# - platform: state
|
||||||
entity_id: sensor.harmony_activity
|
# entity_id: sensor.harmony_activity
|
||||||
from: 'Fire TV sehen'
|
# from: 'Fire TV sehen'
|
||||||
action:
|
# action:
|
||||||
service: switch.turn_on
|
# service: switch.turn_on
|
||||||
data:
|
# data:
|
||||||
entity_id: switch.wallboard_display
|
# entity_id: switch.wallboard_display
|
||||||
|
|
||||||
- alias: Wallboard Off
|
#- alias: Wallboard Off
|
||||||
trigger:
|
# trigger:
|
||||||
- platform: state
|
# - platform: state
|
||||||
entity_id: binary_sensor.anyone_home
|
# entity_id: binary_sensor.anyone_home
|
||||||
to: 'off'
|
# to: 'off'
|
||||||
- platform: state
|
# - platform: state
|
||||||
entity_id: sensor.harmony_activity
|
# entity_id: sensor.harmony_activity
|
||||||
to: 'Fire TV sehen'
|
# to: 'Fire TV sehen'
|
||||||
action:
|
# action:
|
||||||
service: switch.turn_off
|
# service: switch.turn_off
|
||||||
data:
|
# data:
|
||||||
entity_id: switch.wallboard_display
|
# entity_id: switch.wallboard_display
|
|
@ -1,9 +1,12 @@
|
||||||
- platform: reolink_dev
|
- platform: generic
|
||||||
host: !secret cam_livingroom_ip
|
name: deebot_dobby_live_map
|
||||||
username: !secret cam_livingroom_user
|
still_image_url: "https://hass.f-brinker.de/local/vacuums/Dobby_liveMap.png"
|
||||||
password: !secret cam_livingroom_password
|
#- platform: reolink_dev
|
||||||
name: livingroom
|
# host: !secret cam_livingroom_ip
|
||||||
stream: main
|
# username: !secret cam_livingroom_user
|
||||||
protocol: rtmp
|
# password: !secret cam_livingroom_password
|
||||||
channel: 0
|
# name: livingroom
|
||||||
scan_interval: 30
|
# stream: main
|
||||||
|
# protocol: rtmp
|
||||||
|
# channel: 0
|
||||||
|
# scan_interval: 30
|
|
@ -1,4 +1,11 @@
|
||||||
person.jenny:
|
person.jenny:
|
||||||
entity_picture: "/local/avatars/jenny-bty.jpg"
|
entity_picture: "/local/avatars/jenny-bty.jpg"
|
||||||
person.florian:
|
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
|
|
@ -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
|
- platform: group
|
||||||
name: Ambilight
|
name: Küchen-Theke
|
||||||
host: !secret ambilight_ip
|
entities:
|
||||||
hdmi_priority: 900
|
- light.kitchen1
|
||||||
|
- light.kitchen2
|
||||||
|
- platform: group
|
||||||
|
name: Esstisch
|
||||||
|
entities:
|
||||||
|
- light.dining1
|
||||||
|
- light.dining2
|
||||||
|
# - platform: hyperion
|
||||||
|
# name: Ambilight
|
||||||
|
# host: !secret ambilight_ip
|
||||||
- platform: group
|
- platform: group
|
||||||
name: onAir
|
name: onAir
|
||||||
entities:
|
entities:
|
||||||
|
@ -9,7 +18,7 @@
|
||||||
- light.innr_gu10_rgb_2
|
- light.innr_gu10_rgb_2
|
||||||
- platform: switch
|
- platform: switch
|
||||||
name: Lichterkette
|
name: Lichterkette
|
||||||
entity_id: switch.innr_steckdose
|
entity_id: switch.garden_chain_of_lights
|
||||||
- platform: switch
|
- platform: switch
|
||||||
name: Stimmungslicht
|
name: Stimmungslicht
|
||||||
entity_id: switch.livingroom_stimmungslicht
|
entity_id: switch.moodlight
|
|
@ -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
|
count: 2
|
||||||
scan_interval: 15
|
scan_interval: 15
|
||||||
|
|
||||||
|
- platform: ping
|
||||||
|
name: desktop_jenny_ping
|
||||||
|
host: !secret desktop_jenny_ip
|
||||||
|
count: 2
|
||||||
|
scan_interval: 15
|
||||||
|
|
||||||
- platform: template
|
- platform: template
|
||||||
sensors:
|
sensors:
|
||||||
anyone_home:
|
anyone_home:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
- platform: command_line
|
# - platform: command_line
|
||||||
name: Wallboard HDMI Status
|
# name: Wallboard HDMI Status
|
||||||
command: !secret wallboard_hdmi_status_cmd
|
# command: !secret wallboard_hdmi_status_cmd
|
||||||
payload_on: display_power=1
|
# payload_on: display_power=1
|
||||||
payload_off: display_power=0
|
# payload_off: display_power=0
|
||||||
scan_interval: 60
|
# 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:
|
turn_off:
|
||||||
service: script.dummy
|
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:
|
onair_lamp_recording:
|
||||||
value_template: "{{ is_state('input_boolean.onair_lamp_recording', 'on') }}"
|
value_template: "{{ is_state('input_boolean.onair_lamp_recording', 'on') }}"
|
||||||
turn_on:
|
turn_on:
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
- platform: template
|
#- platform: template
|
||||||
switches:
|
# switches:
|
||||||
|
# wallboard_display:
|
||||||
wallboard_display:
|
# friendly_name: Wallboard Display Toggle
|
||||||
friendly_name: Wallboard Display Toggle
|
# value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}"
|
||||||
value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}"
|
# turn_on:
|
||||||
turn_on:
|
# service: script.wallboard_hdmi_on
|
||||||
service: script.wallboard_hdmi_on
|
# turn_off:
|
||||||
turn_off:
|
# service: script.wallboard_hdmi_off
|
||||||
service: script.wallboard_hdmi_off
|
|
|
@ -1,20 +1,15 @@
|
||||||
icon: mdi:cellphone-link
|
icon: mdi:cellphone-link
|
||||||
path: devices
|
path: devices
|
||||||
cards:
|
cards:
|
||||||
- type: entities
|
|
||||||
entities:
|
|
||||||
- entity: sensor.myip
|
|
||||||
name: Öffentliche IP
|
|
||||||
icon: mdi:earth
|
|
||||||
- type: horizontal-stack
|
- type: horizontal-stack
|
||||||
cards:
|
cards:
|
||||||
- type: entities
|
- type: entities
|
||||||
title: Florian
|
title: Florian
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
entities:
|
entities:
|
||||||
- entity: device_tracker.pixel_4
|
- entity: device_tracker.pixely
|
||||||
name: Standort
|
name: Standort
|
||||||
- entity: sensor.battery_level
|
- entity: sensor.pixely_akkufullstand
|
||||||
name: Handy-Akku
|
name: Handy-Akku
|
||||||
icon: mdi:battery
|
icon: mdi:battery
|
||||||
- type: entities
|
- type: entities
|
||||||
|
@ -29,28 +24,23 @@ cards:
|
||||||
- type: horizontal-stack
|
- type: horizontal-stack
|
||||||
cards:
|
cards:
|
||||||
- type: entities
|
- type: entities
|
||||||
title: Wake On Lan
|
title: WOL Florian
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
entities:
|
entities:
|
||||||
- entity: switch.desktop_wol
|
- entity: switch.desktop_wol
|
||||||
name: Desktop-PC
|
name: Desktop-PC
|
||||||
icon: mdi:desktop-classic
|
icon: mdi:desktop-classic
|
||||||
- type: entities
|
- type: entities
|
||||||
title: Wallboard
|
title: WOL Jenny
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
entities:
|
entities:
|
||||||
- entity: switch.wallboard_display
|
- entity: switch.desktop_jenny_wol
|
||||||
name: Display
|
name: Desktop-PC Ronny
|
||||||
icon: mdi:tablet
|
icon: mdi:desktop-classic
|
||||||
- type: entities
|
- type: entities
|
||||||
title: Home Assistant Slaves
|
title: Wallboard
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
entities:
|
entities:
|
||||||
- type: weblink
|
- entity: switch.wallboard_display
|
||||||
name: Livingroom
|
name: Display
|
||||||
url: !secret url_haslave_livingroom
|
icon: mdi:tablet
|
||||||
icon: mdi:home-assistant
|
|
||||||
- type: weblink
|
|
||||||
name: Office
|
|
||||||
url: !secret url_haslave_office
|
|
||||||
icon: mdi:home-assistant
|
|
|
@ -1,6 +1,7 @@
|
||||||
#icon: mdi:home-variant-outline
|
#icon: mdi:home-variant-outline
|
||||||
title: Haus
|
title: Haus
|
||||||
path: floor
|
path: floor
|
||||||
|
icon: 'mdi:floor-plan'
|
||||||
panel: true
|
panel: true
|
||||||
cards:
|
cards:
|
||||||
- type: picture-elements
|
- type: picture-elements
|
||||||
|
|
|
@ -1,151 +1,103 @@
|
||||||
icon: mdi:water-percent
|
icon: mdi:water-percent
|
||||||
path: humidity
|
path: humidity
|
||||||
cards:
|
cards:
|
||||||
- type: vertical-stack
|
|
||||||
|
- type: grid
|
||||||
|
title: Wohnzimmer
|
||||||
|
columns: 2
|
||||||
|
square: true
|
||||||
cards:
|
cards:
|
||||||
- type: markdown
|
- type: sensor
|
||||||
content: "### Elternbad"
|
entity: sensor.lumi_livingroom_humidity
|
||||||
- type: horizontal-stack
|
name: Luftfeuchtigkeit
|
||||||
cards:
|
graph: line
|
||||||
- type: sensor
|
unit: "%"
|
||||||
entity: sensor.hygro_bathroom_parents_humidity
|
detail: 2
|
||||||
name: Luftfeuchtigkeit
|
hours_to_show: 24
|
||||||
graph: line
|
- type: sensor
|
||||||
unit: "%"
|
entity: sensor.lumi_livingroom_temperature
|
||||||
detail: 2
|
name: Temperatur
|
||||||
hours_to_show: 12
|
graph: line
|
||||||
- type: sensor
|
unit: °C
|
||||||
entity: sensor.hygro_bathroom_parents_temperature
|
detail: 2
|
||||||
name: Temperatur
|
hours_to_show: 24
|
||||||
graph: line
|
|
||||||
unit: °C
|
- type: grid
|
||||||
detail: 2
|
title: Schlafzimmer
|
||||||
hours_to_show: 12
|
columns: 2
|
||||||
- type: markdown
|
square: true
|
||||||
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
|
|
||||||
cards:
|
cards:
|
||||||
- type: markdown
|
- type: sensor
|
||||||
content: "### Wohnzimmer"
|
entity: sensor.lumi_bedroom_humidity
|
||||||
- type: horizontal-stack
|
name: Luftfeuchtigkeit
|
||||||
cards:
|
graph: line
|
||||||
- type: sensor
|
unit: "%"
|
||||||
entity: sensor.lumi_livingroom_humidity
|
detail: 2
|
||||||
name: Luftfeuchtigkeit
|
hours_to_show: 24
|
||||||
graph: line
|
- type: sensor
|
||||||
unit: "%"
|
entity: sensor.lumi_bedroom_temperature
|
||||||
detail: 2
|
name: Temperatur
|
||||||
hours_to_show: 12
|
graph: line
|
||||||
- type: sensor
|
unit: °C
|
||||||
entity: sensor.lumi_livingroom_temperature
|
detail: 2
|
||||||
name: Temperatur
|
hours_to_show: 24
|
||||||
graph: line
|
|
||||||
unit: °C
|
- type: grid
|
||||||
detail: 2
|
title: Gästezimmer
|
||||||
hours_to_show: 12
|
columns: 2
|
||||||
- type: markdown
|
square: true
|
||||||
content: "### Hauswirtschaftsraum"
|
cards:
|
||||||
- type: horizontal-stack
|
- type: sensor
|
||||||
cards:
|
entity: sensor.lumi_guestroom_humidity
|
||||||
- type: sensor
|
name: Luftfeuchtigkeit
|
||||||
entity: sensor.hygro_hwr_humidity
|
graph: line
|
||||||
name: Luftfeuchtigkeit
|
unit: "%"
|
||||||
graph: line
|
detail: 2
|
||||||
unit: "%"
|
hours_to_show: 24
|
||||||
detail: 2
|
- type: sensor
|
||||||
hours_to_show: 12
|
entity: sensor.lumi_guestroom_temperature
|
||||||
- type: sensor
|
name: Temperatur
|
||||||
entity: sensor.hygro_hwr_temperature
|
graph: line
|
||||||
name: Temperatur
|
unit: °C
|
||||||
graph: line
|
detail: 2
|
||||||
unit: °C
|
hours_to_show: 24
|
||||||
detail: 2
|
|
||||||
hours_to_show: 12
|
- type: grid
|
||||||
- type: markdown
|
title: Büro
|
||||||
content: "### Büro"
|
columns: 2
|
||||||
- type: horizontal-stack
|
square: true
|
||||||
cards:
|
cards:
|
||||||
- type: sensor
|
- type: sensor
|
||||||
entity: sensor.lumi_office_humidity
|
entity: sensor.lumi_office_humidity
|
||||||
name: Luftfeuchtigkeit
|
name: Luftfeuchtigkeit
|
||||||
graph: line
|
graph: line
|
||||||
unit: "%"
|
unit: "%"
|
||||||
detail: 2
|
detail: 2
|
||||||
hours_to_show: 12
|
hours_to_show: 24
|
||||||
- type: sensor
|
- type: sensor
|
||||||
entity: sensor.lumi_office_temperature
|
entity: sensor.lumi_office_temperature
|
||||||
name: Temperatur
|
name: Temperatur
|
||||||
graph: line
|
graph: line
|
||||||
unit: °C
|
unit: °C
|
||||||
detail: 2
|
detail: 2
|
||||||
hours_to_show: 12
|
hours_to_show: 24
|
||||||
- type: markdown
|
|
||||||
content: "### Dachboden"
|
- type: grid
|
||||||
- type: horizontal-stack
|
title: Dachboden
|
||||||
cards:
|
columns: 2
|
||||||
- type: sensor
|
square: true
|
||||||
entity: sensor.attic_humidity_2
|
cards:
|
||||||
name: Luftfeuchtigkeit
|
- type: sensor
|
||||||
graph: line
|
entity: sensor.attic_humidity_2
|
||||||
unit: "%"
|
name: Luftfeuchtigkeit
|
||||||
detail: 2
|
graph: line
|
||||||
hours_to_show: 12
|
unit: "%"
|
||||||
- type: sensor
|
detail: 2
|
||||||
entity: sensor.attic_temperature_2
|
hours_to_show: 24
|
||||||
name: Temperatur
|
- type: sensor
|
||||||
graph: line
|
entity: sensor.attic_temperature_2
|
||||||
unit: °C
|
name: Temperatur
|
||||||
detail: 2
|
graph: line
|
||||||
hours_to_show: 12
|
unit: °C
|
||||||
|
detail: 2
|
||||||
|
hours_to_show: 24
|
||||||
|
|
|
@ -1,24 +1,42 @@
|
||||||
title: Lichter
|
title: Lichter
|
||||||
path: lights
|
path: lights
|
||||||
|
icon: 'mdi:lightbulb'
|
||||||
cards:
|
cards:
|
||||||
- type: light
|
- type: grid
|
||||||
entity: light.kuchen_theke
|
title: Erdgeschoss
|
||||||
name: Küchen-Theke
|
columns: 3
|
||||||
- type: light
|
suqare: true
|
||||||
entity: light.stimmungslicht
|
cards:
|
||||||
name: Stimmungslicht
|
- type: light
|
||||||
- type: light
|
entity: light.kuchen_theke
|
||||||
entity: light.ambilight
|
name: Küchen-Theke
|
||||||
name: Ambilight
|
- type: light
|
||||||
- type: light
|
entity: light.esstisch
|
||||||
entity: light.lichterkette
|
name: Esstisch
|
||||||
name: Lichterkette (Garten)
|
- type: light
|
||||||
- type: light
|
entity: light.stimmungslicht
|
||||||
entity: light.esstisch
|
name: Stimmungslicht
|
||||||
name: Esstisch
|
- type: light
|
||||||
- type: light
|
entity: light.ambilight
|
||||||
entity: light.tint_rgb_gu10_1
|
name: Ambilight
|
||||||
name: Kinderbad
|
- 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
|
- type: vertical-stack
|
||||||
title: Studio
|
title: Studio
|
||||||
|
@ -29,19 +47,11 @@ cards:
|
||||||
- type: entities
|
- type: entities
|
||||||
show_header_toggle: false
|
show_header_toggle: false
|
||||||
entities:
|
entities:
|
||||||
|
- entity: light.philips_iris
|
||||||
|
name: Iris
|
||||||
- entity: switch.onair_lamp_recording
|
- entity: switch.onair_lamp_recording
|
||||||
name: Studio Aufnahme
|
name: Studio Aufnahme
|
||||||
icon: mdi:camera-rear
|
icon: mdi:camera-rear
|
||||||
- entity: switch.osram_plug_01_57b6060a_on_off
|
- entity: switch.osram_plug_01_57b6060a_on_off
|
||||||
name: Studio Ringlicht
|
name: Studio Ringlicht
|
||||||
icon: mdi:checkbox-blank-circle-outline
|
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
|
title: Media
|
||||||
path: livingroom
|
path: media
|
||||||
|
icon: 'mdi:theater'
|
||||||
cards:
|
cards:
|
||||||
# Lights
|
- type: media-control
|
||||||
- type: horizontal-stack
|
entity: media_player.spotify
|
||||||
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
|
|
||||||
|
|
||||||
# Denon + Harmony
|
# Denon + Harmony
|
||||||
- type: vertical-stack
|
- type: vertical-stack
|
||||||
|
@ -45,14 +34,11 @@ cards:
|
||||||
icon: mdi:bluetooth-audio
|
icon: mdi:bluetooth-audio
|
||||||
- entity: switch.harmony_steamlink
|
- entity: switch.harmony_steamlink
|
||||||
icon: mdi:steam
|
icon: mdi:steam
|
||||||
- entity: switch.harmony_playstation
|
|
||||||
icon: mdi:playstation
|
|
||||||
state_image:
|
state_image:
|
||||||
"PowerOff": /local/images/power.jpg
|
"PowerOff": /local/images/power.jpg
|
||||||
"Fire TV sehen": /local/images/firetv.jpg
|
"Fire TV sehen": /local/images/firetv.jpg
|
||||||
"Musik Bluetooth": /local/images/music.jpg
|
"Musik Bluetooth": /local/images/music.jpg
|
||||||
"SteamLink": /local/images/steamlink.jpg
|
"SteamLink": /local/images/steamlink.jpg
|
||||||
"PlayStation": /local/images/playstation.jpg
|
|
||||||
entity: sensor.harmony_activity
|
entity: sensor.harmony_activity
|
||||||
|
|
||||||
# Spotify
|
# Spotify
|
|
@ -3,6 +3,10 @@ path: network
|
||||||
cards:
|
cards:
|
||||||
- type: vertical-stack
|
- type: vertical-stack
|
||||||
cards:
|
cards:
|
||||||
|
- type: entity
|
||||||
|
entity: sensor.myip
|
||||||
|
name: Öffentliche IP
|
||||||
|
icon: mdi:earth
|
||||||
- type: gauge
|
- type: gauge
|
||||||
entity: sensor.adguard_average_processing_speed
|
entity: sensor.adguard_average_processing_speed
|
||||||
max: 100
|
max: 100
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
title: Übersicht
|
title: Übersicht
|
||||||
path: overview
|
path: overview
|
||||||
|
icon: "mdi:tablet-dashboard"
|
||||||
cards:
|
cards:
|
||||||
- type: conditional
|
- type: conditional
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -16,11 +17,12 @@ cards:
|
||||||
- type: vertical-stack
|
- type: vertical-stack
|
||||||
cards:
|
cards:
|
||||||
- type: custom:weather-card
|
- type: custom:weather-card
|
||||||
entity: weather.openweathermap
|
|
||||||
name: Wetter
|
name: Wetter
|
||||||
- type: iframe
|
entity: weather.openweathermap
|
||||||
url: !secret iframe_windy
|
icons: "/local/lovelace/custom/weather-card/icons/"
|
||||||
aspect_ratio: 75%
|
# - type: iframe
|
||||||
|
# url: !secret iframe_windy
|
||||||
|
# aspect_ratio: 75%
|
||||||
# - type: iframe
|
# - type: iframe
|
||||||
# url: !secret iframe_earth
|
# url: !secret iframe_earth
|
||||||
# aspect_ratio: 75%
|
# aspect_ratio: 75%
|
||||||
|
@ -45,7 +47,7 @@ cards:
|
||||||
- person.florian
|
- person.florian
|
||||||
- person.jenny
|
- person.jenny
|
||||||
- type: map
|
- type: map
|
||||||
aspect_ratio: 75%
|
aspect_ratio: 60%
|
||||||
default_zoom: 15
|
entities:
|
||||||
entities:
|
- device_tracker.sm_g985f
|
||||||
- zone.home
|
- device_tracker.pixel_4
|
|
@ -1,8 +1,48 @@
|
||||||
title: Mäheroboter
|
title: Roboter
|
||||||
path: mower
|
path: robots
|
||||||
badges: []
|
badges: []
|
||||||
icon: 'mdi:robot-vacuum-variant'
|
icon: 'mdi:robot-vacuum-variant'
|
||||||
cards:
|
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
|
- type: vertical-stack
|
||||||
cards:
|
cards:
|
||||||
- elements:
|
- elements:
|
|
@ -1,5 +0,0 @@
|
||||||
icon: mdi:spotify
|
|
||||||
path: spotify
|
|
||||||
cards:
|
|
||||||
- type: media-control
|
|
||||||
entity: media_player.spotify
|
|
|
@ -1,5 +1,7 @@
|
||||||
homeassistant:
|
homeassistant:
|
||||||
name: Home
|
name: Home
|
||||||
|
internal_url: !secret url_internal
|
||||||
|
external_url: !secret url_external
|
||||||
latitude: !secret home_lat
|
latitude: !secret home_lat
|
||||||
longitude: !secret home_long
|
longitude: !secret home_long
|
||||||
elevation: !secret home_elevation
|
elevation: !secret home_elevation
|
||||||
|
@ -15,12 +17,11 @@ homeassistant:
|
||||||
- 192.168.0.0/16
|
- 192.168.0.0/16
|
||||||
- fd00::/8
|
- fd00::/8
|
||||||
# landroid
|
# landroid
|
||||||
packages: !include_dir_named packages
|
#packages: !include_dir_named packages
|
||||||
|
|
||||||
config:
|
config:
|
||||||
conversation:
|
conversation:
|
||||||
device_tracker:
|
dhcp:
|
||||||
#discovery:
|
|
||||||
history:
|
history:
|
||||||
logbook:
|
logbook:
|
||||||
map:
|
map:
|
||||||
|
@ -35,31 +36,36 @@ http:
|
||||||
ip_ban_enabled: true
|
ip_ban_enabled: true
|
||||||
login_attempts_threshold: 5
|
login_attempts_threshold: 5
|
||||||
use_x_forwarded_for: true
|
use_x_forwarded_for: true
|
||||||
base_url: !secret url_base
|
|
||||||
trusted_proxies:
|
trusted_proxies:
|
||||||
- 127.0.0.1
|
- 127.0.0.1
|
||||||
- ::1
|
- ::1
|
||||||
|
# traefik.web
|
||||||
|
- 172.19.0.0/24
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
themes: !include_dir_merge_named themes/
|
themes: !include_dir_merge_named themes/
|
||||||
|
|
||||||
lovelace:
|
lovelace:
|
||||||
mode: yaml
|
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:
|
tts:
|
||||||
- platform: google_translate
|
- platform: google_translate
|
||||||
service_name: google_say
|
service_name: google_say
|
||||||
|
|
||||||
zha:
|
zha:
|
||||||
usb_path: /dev/ttyACM0
|
|
||||||
radio_type: deconz
|
|
||||||
database_path: /config/zigbee.db
|
database_path: /config/zigbee.db
|
||||||
|
|
||||||
emulated_roku:
|
|
||||||
servers:
|
|
||||||
- name: Home Assistant
|
|
||||||
listen_port: !secret roku_port
|
|
||||||
|
|
||||||
mqtt:
|
mqtt:
|
||||||
broker: !secret mqtt_broker_ip
|
broker: !secret mqtt_broker_ip
|
||||||
username: !secret mqtt_username
|
username: !secret mqtt_username
|
||||||
|
@ -90,32 +96,18 @@ notify:
|
||||||
- platform: command_line
|
- platform: command_line
|
||||||
name: alexa_all
|
name: alexa_all
|
||||||
command: "/config/alexa_wrapper.sh -d '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:
|
person:
|
||||||
- name: Jenny
|
- name: Jenny
|
||||||
id: jenny
|
id: jenny
|
||||||
|
user_id: !secret userId_jenny
|
||||||
device_trackers:
|
device_trackers:
|
||||||
- device_tracker.sm_g985f
|
- device_tracker.sm_g985f
|
||||||
- name: Florian
|
- name: Florian
|
||||||
id: florian
|
id: florian
|
||||||
|
user_id: !secret userId_florian
|
||||||
device_trackers:
|
device_trackers:
|
||||||
- device_tracker.pixel_4
|
- device_tracker.pixely
|
||||||
|
|
||||||
zone:
|
zone:
|
||||||
- name: !secret work_name_f
|
- name: !secret work_name_f
|
||||||
|
@ -129,10 +121,6 @@ zone:
|
||||||
radius: 500
|
radius: 500
|
||||||
icon: mdi:briefcase
|
icon: mdi:briefcase
|
||||||
|
|
||||||
weather:
|
|
||||||
- platform: openweathermap
|
|
||||||
api_key: !secret openweathermap
|
|
||||||
|
|
||||||
ffmpeg:
|
ffmpeg:
|
||||||
ffmpeg_bin: /usr/bin/ffmpeg
|
ffmpeg_bin: /usr/bin/ffmpeg
|
||||||
|
|
||||||
|
@ -198,20 +186,27 @@ spotify:
|
||||||
client_id: !secret spotify_client_id
|
client_id: !secret spotify_client_id
|
||||||
client_secret: !secret spotify_client_secret
|
client_secret: !secret spotify_client_secret
|
||||||
|
|
||||||
ecovacs:
|
deebot:
|
||||||
username: !secret ecovacs_user
|
username: !secret ecovacs_user
|
||||||
password: !secret ecovacs_password
|
password: !secret ecovacs_password
|
||||||
country: de
|
country: de
|
||||||
continent: eu
|
continent: eu
|
||||||
|
deviceid:
|
||||||
|
- !secret ecovacs_serial_dobby
|
||||||
|
live_map: true
|
||||||
|
show_color_rooms: true
|
||||||
|
livemappath: 'www/vacuums/'
|
||||||
|
|
||||||
#tplink:
|
fontawesome:
|
||||||
# discovery: false
|
regular:
|
||||||
# switch:
|
solid:
|
||||||
# - host: !secret tplink_ip
|
brands:
|
||||||
|
|
||||||
# External config files
|
# External config files
|
||||||
alert: !include_dir_merge_named config/alerts/
|
|
||||||
alexa: !include config/alexa.yaml
|
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/
|
automation: !include_dir_merge_list config/automations/
|
||||||
binary_sensor: !include_dir_merge_list config/sensors_binary/
|
binary_sensor: !include_dir_merge_list config/sensors_binary/
|
||||||
camera: !include config/cameras.yaml
|
camera: !include config/cameras.yaml
|
||||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"domain": "fontawesome",
|
||||||
|
"name": "Fontawesome icons",
|
||||||
|
"documentation": "",
|
||||||
|
"dependencies": ["frontend"],
|
||||||
|
"codeowners": [],
|
||||||
|
"requirements": [],
|
||||||
|
"config_flow": true
|
||||||
|
}
|
|
@ -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",
|
"icon": "mdi:battery",
|
||||||
"unit": "%",
|
"unit": "%",
|
||||||
"device_class": None,
|
"device_class": "battery",
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"state": {"error_description": "state", "error": "error_id"},
|
"state": {"error_description": "state", "error": "error_id"},
|
||||||
"icon": None,
|
"icon": "mdi:alert",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"device_class": None,
|
"device_class": None,
|
||||||
},
|
},
|
||||||
|
@ -83,6 +83,8 @@ API_WORX_SENSORS = {
|
||||||
"rain_delay": "raindelay",
|
"rain_delay": "raindelay",
|
||||||
"schedule_variation": "timeextension",
|
"schedule_variation": "timeextension",
|
||||||
"firmware": "firmware_version",
|
"firmware": "firmware_version",
|
||||||
|
"serial": "serial",
|
||||||
|
"mac": "mac",
|
||||||
},
|
},
|
||||||
"icon": None,
|
"icon": None,
|
||||||
"unit": None,
|
"unit": None,
|
||||||
|
@ -130,7 +132,7 @@ async def async_setup(hass, config):
|
||||||
async def handle_start(call):
|
async def handle_start(call):
|
||||||
"""Handle start service call."""
|
"""Handle start service call."""
|
||||||
if "id" in call.data:
|
if "id" in call.data:
|
||||||
ID = call.data["id"]
|
ID = int(call.data["id"])
|
||||||
|
|
||||||
for cli in client:
|
for cli in client:
|
||||||
attrs = vars(cli)
|
attrs = vars(cli)
|
||||||
|
@ -144,7 +146,7 @@ async def async_setup(hass, config):
|
||||||
async def handle_pause(call):
|
async def handle_pause(call):
|
||||||
"""Handle pause service call."""
|
"""Handle pause service call."""
|
||||||
if "id" in call.data:
|
if "id" in call.data:
|
||||||
ID = call.data["id"]
|
ID = int(call.data["id"])
|
||||||
|
|
||||||
for cli in client:
|
for cli in client:
|
||||||
attrs = vars(cli)
|
attrs = vars(cli)
|
||||||
|
@ -158,7 +160,7 @@ async def async_setup(hass, config):
|
||||||
async def handle_home(call):
|
async def handle_home(call):
|
||||||
"""Handle pause service call."""
|
"""Handle pause service call."""
|
||||||
if "id" in call.data:
|
if "id" in call.data:
|
||||||
ID = call.data["id"]
|
ID = int(call.data["id"])
|
||||||
|
|
||||||
for cli in client:
|
for cli in client:
|
||||||
attrs = vars(cli)
|
attrs = vars(cli)
|
||||||
|
@ -177,31 +179,46 @@ async def async_setup(hass, config):
|
||||||
|
|
||||||
if "id" in call.data:
|
if "id" in call.data:
|
||||||
_LOGGER.debug("Data from Home Assistant: %s", call.data["id"])
|
_LOGGER.debug("Data from Home Assistant: %s", call.data["id"])
|
||||||
|
|
||||||
for cli in client:
|
for cli in client:
|
||||||
attrs = vars(cli)
|
attrs = vars(cli)
|
||||||
if (attrs["id"] == call.data["id"]):
|
if (attrs["id"] == int(call.data["id"])):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
id += 1
|
id += 1
|
||||||
|
|
||||||
if "raindelay" in call.data:
|
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"])
|
_LOGGER.debug("Setting rain_delay for %s to %s", client[id].name, call.data["raindelay"])
|
||||||
sendData = True
|
sendData = True
|
||||||
|
|
||||||
if "timeextension" in call.data:
|
if "timeextension" in call.data:
|
||||||
tmpdata["sc"] = {}
|
tmpdata["sc"] = {}
|
||||||
tmpdata["sc"]["p"] = call.data["timeextension"]
|
tmpdata["sc"]["p"] = int(call.data["timeextension"])
|
||||||
data = json.dumps(tmpdata)
|
data = json.dumps(tmpdata)
|
||||||
_LOGGER.debug("Setting time_extension for %s to %s", client[id].name, call.data["timeextension"])
|
_LOGGER.debug("Setting time_extension for %s to %s", client[id].name, call.data["timeextension"])
|
||||||
sendData = True
|
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:
|
if sendData:
|
||||||
data = json.dumps(tmpdata)
|
data = json.dumps(tmpdata)
|
||||||
_LOGGER.debug("Sending: %s", data)
|
_LOGGER.debug("Sending: %s", data)
|
||||||
client[id].sendData(data)
|
client[id].sendData(data)
|
||||||
|
|
||||||
hass.services.async_register(DOMAIN, SERVICE_CONFIG, handle_config)
|
hass.services.async_register(DOMAIN, SERVICE_CONFIG, handle_config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
{
|
{
|
||||||
"domain": "landroid_cloud",
|
"domain": "landroid_cloud",
|
||||||
"name": "Worx Landroid Cloud",
|
"name": "Worx Landroid Cloud",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/landroid_cloud/",
|
"documentation": "https://github.com/MTrab/landroid_cloud/blob/master/README.md",
|
||||||
"requirements": ["pyworxcloud==1.2.17"],
|
"issue_tracker": "https://github.com/MTrab/landroid_cloud/issues",
|
||||||
|
"requirements": ["pyworxcloud==1.2.21"],
|
||||||
|
"version": "1.6.5",
|
||||||
"codeowners": ["@MTrab"]
|
"codeowners": ["@MTrab"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,127 +1,132 @@
|
||||||
"""Support for monitoring Worx Landroid Sensors."""
|
"""Support for monitoring Worx Landroid Sensors."""
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components import sensor
|
from homeassistant.components import sensor
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.icon import icon_for_battery_level
|
||||||
from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL
|
|
||||||
|
from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
STATE_INITIALIZING = "Initializing"
|
|
||||||
STATE_OFFLINE = "Offline"
|
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."""
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
if discovery_info is None:
|
"""Set up the available sensors for Worx Landroid."""
|
||||||
return
|
if discovery_info is None:
|
||||||
|
return
|
||||||
entities = []
|
|
||||||
|
entities = []
|
||||||
info = discovery_info[0]
|
|
||||||
for tSensor in API_WORX_SENSORS:
|
info = discovery_info[0]
|
||||||
name = "{}_{}".format(info["name"].lower(), tSensor.lower())
|
for tSensor in API_WORX_SENSORS:
|
||||||
friendly_name = "{} {}".format(info["friendly"], tSensor)
|
name = "{}_{}".format(info["name"].lower(), tSensor.lower())
|
||||||
dev_id = info["id"]
|
friendly_name = "{} {}".format(info["friendly"], tSensor)
|
||||||
api = hass.data[LANDROID_API][dev_id]
|
dev_id = info["id"]
|
||||||
sensor_type = tSensor
|
api = hass.data[LANDROID_API][dev_id]
|
||||||
_LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"])
|
sensor_type = tSensor
|
||||||
entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id)
|
_LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"])
|
||||||
entities.append(entity)
|
entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id)
|
||||||
|
entities.append(entity)
|
||||||
async_add_entities(entities, True)
|
|
||||||
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
class LandroidSensor(Entity):
|
|
||||||
"""Class to create and populate a Landroid Sensor."""
|
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."""
|
def __init__(self, api, name, sensor_type, friendly_name, dev_id):
|
||||||
|
"""Init new sensor."""
|
||||||
self._api = api
|
|
||||||
self._attributes = {}
|
self._api = api
|
||||||
self._available = False
|
self._attributes = {}
|
||||||
self._name = friendly_name
|
self._available = False
|
||||||
self._state = STATE_INITIALIZING
|
self._name = friendly_name
|
||||||
self._sensor_type = sensor_type
|
self._state = STATE_INITIALIZING
|
||||||
self._dev_id = dev_id
|
self._sensor_type = sensor_type
|
||||||
self.entity_id = sensor.ENTITY_ID_FORMAT.format(name)
|
self._dev_id = dev_id
|
||||||
|
self.entity_id = sensor.ENTITY_ID_FORMAT.format(name)
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
@property
|
||||||
"""Return True if entity is available."""
|
def available(self) -> bool:
|
||||||
return self._available
|
"""Return True if entity is available."""
|
||||||
|
return self._available
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
@property
|
||||||
"""Return sensor attributes."""
|
def device_state_attributes(self):
|
||||||
return self._attributes
|
"""Return sensor attributes."""
|
||||||
|
return self._attributes
|
||||||
@property
|
|
||||||
def name(self):
|
@property
|
||||||
"""Return the name of the sensor."""
|
def name(self):
|
||||||
return self._name
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
@property
|
|
||||||
def unit_of_measurement(self):
|
@property
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
def unit_of_measurement(self):
|
||||||
return API_WORX_SENSORS[self._sensor_type]["unit"]
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
|
return API_WORX_SENSORS[self._sensor_type]["unit"]
|
||||||
@property
|
|
||||||
def icon(self):
|
@property
|
||||||
"""Icon to use in the frontend."""
|
def icon(self):
|
||||||
return API_WORX_SENSORS[self._sensor_type]["icon"]
|
"""Icon to use in the frontend."""
|
||||||
|
if self._sensor_type == "battery" and isinstance(self.state, int):
|
||||||
@property
|
charging = self._attributes["charging"]
|
||||||
def should_poll(self):
|
return icon_for_battery_level(battery_level=self.state, charging=charging)
|
||||||
"""Return False as entity is updated from the component."""
|
|
||||||
return False
|
return API_WORX_SENSORS[self._sensor_type]["icon"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def should_poll(self):
|
||||||
"""Return sensor state."""
|
"""Return False as entity is updated from the component."""
|
||||||
return self._state
|
return False
|
||||||
|
|
||||||
@callback
|
@property
|
||||||
def update_callback(self):
|
def state(self):
|
||||||
"""Get new data and update state."""
|
"""Return sensor state."""
|
||||||
self.async_schedule_update_ha_state(True)
|
return self._state
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
@callback
|
||||||
"""Connect update callbacks."""
|
def update_callback(self):
|
||||||
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback)
|
"""Get new data and update state."""
|
||||||
|
self.async_schedule_update_ha_state(True)
|
||||||
def _get_data(self):
|
|
||||||
"""Return new data from the api cache."""
|
async def async_added_to_hass(self):
|
||||||
data = self._api.get_data(self._sensor_type)
|
"""Connect update callbacks."""
|
||||||
self._available = True
|
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback)
|
||||||
return data
|
|
||||||
|
def _get_data(self):
|
||||||
async def async_update(self):
|
"""Return new data from the api cache."""
|
||||||
"""Update the sensor."""
|
data = self._api.get_data(self._sensor_type)
|
||||||
_LOGGER.debug("Updating %s", self.entity_id)
|
self._available = True
|
||||||
data = self._get_data()
|
return data
|
||||||
if "state" in data:
|
|
||||||
_LOGGER.debug(data)
|
async def async_update(self):
|
||||||
state = data.pop("state")
|
"""Update the sensor."""
|
||||||
_LOGGER.debug("Mower %s State %s", self._name, state)
|
_LOGGER.debug("Updating %s", self.entity_id)
|
||||||
self._attributes.update(data)
|
data = self._get_data()
|
||||||
self._state = state
|
if "state" in data:
|
||||||
else:
|
_LOGGER.debug(data)
|
||||||
_LOGGER.debug("No data received for %s", self.entity_id)
|
state = data.pop("state")
|
||||||
reachable = self._api._client.online
|
_LOGGER.debug("Mower %s State %s", self._name, state)
|
||||||
if not reachable:
|
self._attributes.update(data)
|
||||||
if "_battery" in self.entity_id:
|
self._state = state
|
||||||
self._state = "Unknown"
|
if "latitude" in self._attributes:
|
||||||
else:
|
if self._attributes["latitude"] == None:
|
||||||
self._state = STATE_OFFLINE
|
del self._attributes["latitude"]
|
||||||
#else:
|
del self._attributes["longitude"]
|
||||||
# attrs = vars(self._api._client)
|
else:
|
||||||
# for item in attrs:
|
_LOGGER.debug("No data received for %s", self.entity_id)
|
||||||
# _LOGGER.debug("%s : %s", item, attrs[item])
|
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:
|
timeextension:
|
||||||
description: Set time extension. Extension in % ranging from -100 to 100
|
description: Set time extension. Extension in % ranging from -100 to 100
|
||||||
example: -23
|
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
|
||||||
|
|
|
@ -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
|
|
@ -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."""
|
"""This component provides support for Reolink IP cameras."""
|
||||||
import logging
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import voluptuous as vol
|
from datetime import datetime
|
||||||
import datetime
|
import logging
|
||||||
|
|
||||||
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 haffmpeg.camera import CameraMjpeg
|
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.components.ffmpeg import DATA_FFMPEG
|
||||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||||
from custom_components.reolink_dev.ReolinkPyPi.camera import ReolinkApi
|
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__)
|
_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
|
@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."""
|
"""Set up a Reolink IP Camera."""
|
||||||
|
|
||||||
host = config.get(CONF_HOST)
|
platform = entity_platform.current_platform.get()
|
||||||
username = config.get(CONF_USERNAME)
|
camera = ReolinkCamera(hass, config_entry)
|
||||||
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)
|
|
||||||
|
|
||||||
session = ReolinkApi(host, channel)
|
platform.async_register_entity_service(
|
||||||
session.login(username, password)
|
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
|
platform.async_register_entity_service(
|
||||||
def handler_enable_ftp(call):
|
SERVICE_SET_BACKLIGHT,
|
||||||
component = hass.data.get(DOMAIN)
|
{
|
||||||
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
|
vol.Required("mode"): cv.string,
|
||||||
|
},
|
||||||
|
SERVICE_SET_BACKLIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
if entity:
|
platform.async_register_entity_service(
|
||||||
entity.enable_ftp_upload()
|
SERVICE_PTZ_CONTROL,
|
||||||
hass.services.async_register(DOMAIN, SERVICE_ENABLE_FTP, handler_enable_ftp)
|
{
|
||||||
|
vol.Required("command"): cv.string,
|
||||||
|
vol.Optional("preset"): cv.positive_int,
|
||||||
|
vol.Optional("speed"): cv.positive_int,
|
||||||
|
},
|
||||||
|
SERVICE_PTZ_CONTROL,
|
||||||
|
)
|
||||||
|
|
||||||
# Event disable FTP
|
async_add_devices([camera])
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class ReolinkCamera(Camera):
|
class ReolinkCamera(ReolinkEntity, Camera):
|
||||||
"""An implementation of a Reolink IP 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."""
|
"""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._hass = hass
|
||||||
self._manager = self._hass.data[DATA_FFMPEG]
|
self._ffmpeg = self._hass.data[DATA_FFMPEG]
|
||||||
|
|
||||||
self._last_update = 0
|
|
||||||
self._last_image = None
|
self._last_image = None
|
||||||
self._last_motion = 0
|
self._ptz_commands = {
|
||||||
self._ftp_state = None
|
"AUTO": "Auto",
|
||||||
self._email_state = None
|
"DOWN": "Down",
|
||||||
self._ir_state = None
|
"FOCUSDEC": "FocusDec",
|
||||||
self._ptzpresets = dict()
|
"FOCUSINC": "FocusInc",
|
||||||
self._state = STATE_IDLE
|
"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
|
@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."""
|
"""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:
|
for key, value in self._backlight_modes.items():
|
||||||
attrs["last_motion"] = self._last_motion
|
if value == self._base.api.backlight_state:
|
||||||
|
attrs["backlight_state"] = key
|
||||||
if self._last_update:
|
|
||||||
attrs["last_update"] = self._last_update
|
|
||||||
|
|
||||||
attrs["ftp_enabled"] = self._ftp_state
|
for key, value in self._daynight_modes.items():
|
||||||
attrs["email_enabled"] = self._email_state
|
if value == self._base.api.daynight_state:
|
||||||
attrs["ir_lights_enabled"] = self._ir_state
|
attrs["daynight_state"] = key
|
||||||
attrs["ptzpresets"] = self._ptzpresets
|
|
||||||
|
if self._base.api.sensitivity_presets:
|
||||||
|
attrs["sensitivity"] = self.get_sensitivity_presets()
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -177,136 +150,72 @@ class ReolinkCamera(Camera):
|
||||||
"""Return supported features."""
|
"""Return supported features."""
|
||||||
return SUPPORT_STREAM
|
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):
|
async def stream_source(self):
|
||||||
"""Return the source of the stream."""
|
"""Return the source of the stream."""
|
||||||
if self._protocol == "rtsp":
|
return await self._base.api.get_stream_source()
|
||||||
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
|
|
||||||
|
|
||||||
async def handle_async_mjpeg_stream(self, request):
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
stream_source = await self.stream_source()
|
stream_source = await self.stream_source()
|
||||||
|
|
||||||
stream = CameraMjpeg(self._manager.binary, loop=self._hass.loop)
|
websession = async_get_clientsession(self._hass)
|
||||||
await stream.open_camera(stream_source)
|
stream_coro = websession.get(stream_source, timeout=10)
|
||||||
|
|
||||||
try:
|
return await async_aiohttp_proxy_web(self._hass, request, stream_coro)
|
||||||
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
|
|
||||||
|
|
||||||
async def async_camera_image(self):
|
async def async_camera_image(self):
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
return self._reolinkSession.snapshot
|
return await self._base.api.get_snapshot()
|
||||||
|
|
||||||
def enable_ftp_upload(self):
|
async def ptz_control(self, command, **kwargs):
|
||||||
"""Enable motion ftp recording in camera."""
|
"""Pass PTZ command to the camera."""
|
||||||
if self._reolinkSession.set_ftp(True):
|
if not self.ptz_support:
|
||||||
self._ftp_state = True
|
_LOGGER.error("PTZ is not supported on this device")
|
||||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
return
|
||||||
|
|
||||||
def disable_ftp_upload(self):
|
await self._base.api.set_ptz_command(
|
||||||
"""Disable motion ftp recording."""
|
command=self._ptz_commands[command], **kwargs
|
||||||
if self._reolinkSession.set_ftp(False):
|
)
|
||||||
self._ftp_state = False
|
|
||||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
|
||||||
|
|
||||||
def enable_email(self):
|
def get_sensitivity_presets(self):
|
||||||
"""Enable email motion detection in camera."""
|
"""Get formatted sensitivity presets."""
|
||||||
if self._reolinkSession.set_email(True):
|
presets = list()
|
||||||
self._email_state = True
|
preset = dict()
|
||||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
|
||||||
|
|
||||||
def disable_email(self):
|
for api_preset in self._base.api.sensitivity_presets:
|
||||||
"""Disable email motion detection."""
|
preset["id"] = api_preset["id"]
|
||||||
if self._reolinkSession.set_email(False):
|
preset["sensitivity"] = api_preset["sensitivity"]
|
||||||
self._email_state = False
|
|
||||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
|
||||||
|
|
||||||
def enable_ir_lights(self):
|
time_string = f'{api_preset["beginHour"]}:{api_preset["beginMin"]}'
|
||||||
"""Enable IR lights."""
|
begin = datetime.strptime(time_string, "%H:%M")
|
||||||
if self._reolinkSession.set_ir_lights(True):
|
preset["begin"] = begin.strftime("%H:%M")
|
||||||
self._ir_state = True
|
|
||||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
|
||||||
|
|
||||||
def disable_ir_lights(self):
|
time_string = f'{api_preset["endHour"]}:{api_preset["endMin"]}'
|
||||||
"""Disable IR lights."""
|
end = datetime.strptime(time_string, "%H:%M")
|
||||||
if self._reolinkSession.set_ir_lights(False):
|
preset["end"] = end.strftime("%H:%M")
|
||||||
self._ir_state = False
|
|
||||||
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
|
|
||||||
|
|
||||||
async def update_motion_state(self):
|
presets.append(preset.copy())
|
||||||
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()
|
|
||||||
|
|
||||||
self._last_update = datetime.datetime.now()
|
return presets
|
||||||
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
|
|
||||||
|
|
||||||
def update(self):
|
async def set_sensitivity(self, sensitivity, **kwargs):
|
||||||
"""Update the data from the camera."""
|
"""Set the sensitivity to the camera."""
|
||||||
try:
|
if "preset" in kwargs:
|
||||||
self._hass.loop.create_task(self.update_motion_state())
|
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
|
async def set_daynight(self, mode):
|
||||||
(datetime.datetime.now() - self._last_update).total_seconds() >= 30):
|
"""Set the day and night mode to the camera."""
|
||||||
self._hass.loop.create_task(self.update_status())
|
await self._base.api.set_daynight(value=self._daynight_modes[mode])
|
||||||
|
|
||||||
except Exception as ex:
|
async def set_backlight(self, mode):
|
||||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
"""Set the backlight mode to the camera."""
|
||||||
|
await self._base.api.set_backlight(value=self._backlight_modes[mode])
|
||||||
|
|
||||||
def disconnect(self, event):
|
async def async_enable_motion_detection(self):
|
||||||
_LOGGER.info("Disconnecting from Reolink camera")
|
"""Predefined camera service implementation."""
|
||||||
self._reolinkSession.logout()
|
self._base.motion_detection_state = True
|
||||||
|
|
||||||
|
async def async_disable_motion_detection(self):
|
||||||
|
"""Predefined camera service implementation."""
|
||||||
|
self._base.motion_detection_state = False
|
||||||
|
|
|
@ -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."""
|
|
@ -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"
|
|
@ -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",
|
"domain": "reolink_dev",
|
||||||
"name": "Reolink IP camera",
|
"name": "Reolink IP camera",
|
||||||
"documentation": "https://www.example.com",
|
"documentation": "https://github.com/fwestenberg/reolink_dev",
|
||||||
"dependencies": ["ffmpeg"],
|
"issue_tracker": "https://github.com/fwestenberg/reolink_dev/issues",
|
||||||
"codeowners": ["@fwestenberg"],
|
"version": "0.15",
|
||||||
"requirements": ["aiosmtpd==1.2"]
|
"requirements": [
|
||||||
}
|
"reolink==0.0.17"
|
||||||
|
],
|
||||||
|
"dependencies": [
|
||||||
|
"ffmpeg",
|
||||||
|
"webhook"
|
||||||
|
],
|
||||||
|
"after_dependencies": [
|
||||||
|
"media_source",
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@fwestenberg"
|
||||||
|
],
|
||||||
|
"config_flow": true,
|
||||||
|
"ssdp": [],
|
||||||
|
"zeroconf": [],
|
||||||
|
"homekit": {}
|
||||||
|
}
|
||||||
|
|
|
@ -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:
|
ptz_control:
|
||||||
description: Enable FTP upload on motion recording.
|
name: Pan/Zoom/Tilt Control
|
||||||
|
description: Execute a PTZ command.
|
||||||
|
target:
|
||||||
|
entity:
|
||||||
|
integration: reolink_dev
|
||||||
|
domain: camera
|
||||||
fields:
|
fields:
|
||||||
entity_id:
|
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'
|
example: 'camera.frontdoor'
|
||||||
|
command:
|
||||||
disable_ftp:
|
description: >-
|
||||||
description: Disable FTP upload on motion recording.
|
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:
|
fields:
|
||||||
entity_id:
|
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'
|
example: 'camera.frontdoor'
|
||||||
|
sensitivity:
|
||||||
enable_email:
|
description: New sensitivity, value between 1 (low sensitivity) and 50 (high sensitivity)
|
||||||
description: Enable email functionality on motion detection.
|
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:
|
fields:
|
||||||
entity_id:
|
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'
|
example: 'camera.frontdoor'
|
||||||
|
mode:
|
||||||
disable_email:
|
description: >-
|
||||||
description: Disable email functionality on motion detection.
|
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:
|
fields:
|
||||||
entity_id:
|
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'
|
example: 'camera.frontdoor'
|
||||||
|
mode:
|
||||||
enable_ir_lights:
|
description: >-
|
||||||
description: Enable the infrared lights (nightvision) of the Reolink camera.
|
The backlight parameter supports the following values:
|
||||||
fields:
|
BACKLIGHTCONTROL: use Backlight Control
|
||||||
entity_id:
|
DYNAMICRANGECONTROL: use Dynamic Range Control
|
||||||
description: Name of the Reolink camera entity to set.
|
OFF: no optimization
|
||||||
example: 'camera.frontdoor'
|
example: DYNAMICRANGECONTROL
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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": "השהיית כבוי תזוזה"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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/
|
# Icons @ https://materialdesignicons.com/
|
||||||
|
# FontAwesome implementation @ https://github.com/thomasloven/hass-fontawesome
|
||||||
title: Making Home Great Again
|
title: Making Home Great Again
|
||||||
|
|
||||||
views:
|
views:
|
||||||
- !include config/views/overview.yaml
|
- !include config/views/overview.yaml
|
||||||
- !include config/views/floorplan.yaml
|
|
||||||
- !include config/views/lights.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/humidity.yaml
|
||||||
- !include config/views/spotify.yaml
|
- !include config/views/floorplan.yaml
|
||||||
- !include config/views/instagram.yaml
|
- !include config/views/instagram.yaml
|
||||||
- !include config/views/devices.yaml
|
- !include config/views/devices.yaml
|
||||||
- !include config/views/landroid.yaml
|
|
||||||
- !include config/views/cctv.yaml
|
- !include config/views/cctv.yaml
|
||||||
- !include config/views/network.yaml
|
- !include config/views/network.yaml
|
||||||
- !include config/views/alerts.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 |
|
@ -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);
|
|
@ -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);
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |