Update HA, add vacuum, grid cards, fixes

This commit is contained in:
Florian Brinker 2021-03-21 18:41:41 +01:00
parent 53c99e4509
commit fb3af71fe3
122 changed files with 12940 additions and 1532 deletions

3
.gitignore vendored
View File

@ -2,6 +2,8 @@
/*
# Allow
!/.vscode/
!*.yaml
!*.jpg
!*.png
@ -23,6 +25,7 @@ android/
ssh-key/
ip_bans.yaml
google_assistant_service_keys.json
secrets.yaml
secrets.js
known_devices.yaml

12
.vscode/home-assistant.code-workspace vendored Normal file
View File

@ -0,0 +1,12 @@
{
"folders": [
{
"path": ".."
}
],
"settings": {
"files.associations": {
"*.yaml": "home-assistant"
}
}
}

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"files.associations": {
"*.yaml": "home-assistant"
}
}

View File

@ -56,4 +56,22 @@ I use the following software, running in docker containers, on my Raspberry Pi:
### Security
* [Yi Home Camera 1080p](https://amzn.to/2SYhoW6) - with [custom firmware and mqtt add-on](https://github.com/fbrinker/yi-hack-mqtt)
# Custom stuff
## Custom Components
* https://github.com/And3rsL/Deebot-for-Home-Assistant
* https://github.com/MTrab/landroid_cloud
* https://github.com/fwestenberg/reolink_dev
* https://github.com/thomasloven/hass-fontawesome
## Lovelace Ressources
* https://github.com/bramkragten/weather-card/
* https://github.com/denysdovhan/vacuum-card
* https://github.com/thomasloven/lovelace-auto-entities
* https://github.com/bradcrc/Now-Playing-Card
* https://github.com/bradcrc/color-lite-card
## Packages
* https://github.com/Barma-lej/halandroid
Work in progress. Not all of them are integrated yet.

View File

@ -30,13 +30,13 @@ garage_door_open:
name: Garagentor geöffnet
message: "Zur Info - Die *Garage* steht noch *offen*!"
done_message: "Zur Info - Die *Garage* ist wieder *geschlossen*."
entity_id: binary_sensor.lumi_garage_door
entity_id: binary_sensor.garage_door
state: "on"
repeat:
- 10
- 30
can_acknowledge: true
skip_first: true
skip_first: false
notifiers:
- telegram_group
- alexa_all

View File

@ -1,6 +1,7 @@
# Display Categories: https://developer.amazon.com/de/docs/device-apis/alexa-discovery.html#display-categories
smart_home:
endpoint: https://api.amazonalexa.com/v3/events
endpoint: https://api.eu.amazonalexa.com/v3/events
locale: de-DE
client_id: !secret alexa_client_id
client_secret: !secret alexa_client_secret
filter:
@ -13,6 +14,7 @@
- light.onair
- light.stimmungslicht
- light.lichterkette
- light.philips_iris
- switch.livingroom_music
- switch.livingroom_netflix
- switch.harmony_firetv
@ -20,6 +22,7 @@
- switch.harmony_playstation
- switch.harmony_denon_power
- switch.desktop_wol
- switch.desktop_jenny_wol
- switch.wallboard_display
- switch.tplink1
- switch.osram_plug_01_57b6060a_on_off
@ -43,7 +46,7 @@
name: Esstisch
description: Esstisch-Lichter
light.office_rgb:
name: Büro
name: Bürolicht
description: Büro - Deckenlampe
light.lichtleiste:
name: Schreibtischlicht
@ -57,6 +60,9 @@
light.onair:
name: Studio-Treppe
description: Studio-Treppenlicht
light.philips_iris:
name: Iris
description: Studio-Stimmungslicht
switch.livingroom_music:
name: Musik
description: Wohnzimmer - Musik
@ -78,6 +84,9 @@
switch.desktop_wol:
name: Computer
description: Computer im Büro
switch.desktop_jenny_wol:
name: Ronny
description: Jennys Computer
switch.wallboard_display:
name: Display
description: Wallboard Display

View File

@ -1,3 +1,69 @@
- alias: Christmas-Tree on
trigger:
- platform: time
at:
- "05:30:00"
- "16:00:00"
- platform: zone
entity_id:
- person.jenny
zone: zone.home
event: enter
action:
service: light.turn_on
data:
entity_id:
- light.weihnachtsbaum
- alias: Christmas-Tree off morning
trigger:
- platform: time
at:
- "08:00:00"
condition:
condition: not
conditions:
- condition: zone
entity_id: person.jenny
zone: zone.home
action:
service: light.turn_off
data:
entity_id:
- light.weihnachtsbaum
- alias: Christmas-Tree off evening
trigger:
- platform: time
at:
- "23:00:00"
action:
service: light.turn_off
data:
entity_id:
- light.weihnachtsbaum
- alias: Christmas-Tree off Presence
trigger:
- platform: zone
entity_id:
- person.jenny
- person.florian
zone: zone.home
event: leave
condition:
- condition: zone
entity_id:
- person.jenny
- person.florian
zone: zone.not_home
action:
service: light.turn_off
data:
entity_id:
- light.weihnachtsbaum
- alias: Ambilight HDMI
trigger:
platform: state

View File

@ -1,25 +1,25 @@
- alias: Wallboard On
trigger:
- platform: state
entity_id: binary_sensor.anyone_home
to: 'on'
- platform: state
entity_id: sensor.harmony_activity
from: 'Fire TV sehen'
action:
service: switch.turn_on
data:
entity_id: switch.wallboard_display
#- alias: Wallboard On
# trigger:
# - platform: state
# entity_id: binary_sensor.anyone_home
# to: 'on'
# - platform: state
# entity_id: sensor.harmony_activity
# from: 'Fire TV sehen'
# action:
# service: switch.turn_on
# data:
# entity_id: switch.wallboard_display
- alias: Wallboard Off
trigger:
- platform: state
entity_id: binary_sensor.anyone_home
to: 'off'
- platform: state
entity_id: sensor.harmony_activity
to: 'Fire TV sehen'
action:
service: switch.turn_off
data:
entity_id: switch.wallboard_display
#- alias: Wallboard Off
# trigger:
# - platform: state
# entity_id: binary_sensor.anyone_home
# to: 'off'
# - platform: state
# entity_id: sensor.harmony_activity
# to: 'Fire TV sehen'
# action:
# service: switch.turn_off
# data:
# entity_id: switch.wallboard_display

View File

@ -1,9 +1,12 @@
- platform: reolink_dev
host: !secret cam_livingroom_ip
username: !secret cam_livingroom_user
password: !secret cam_livingroom_password
name: livingroom
stream: main
protocol: rtmp
channel: 0
scan_interval: 30
- platform: generic
name: deebot_dobby_live_map
still_image_url: "https://hass.f-brinker.de/local/vacuums/Dobby_liveMap.png"
#- platform: reolink_dev
# host: !secret cam_livingroom_ip
# username: !secret cam_livingroom_user
# password: !secret cam_livingroom_password
# name: livingroom
# stream: main
# protocol: rtmp
# channel: 0
# scan_interval: 30

View File

@ -1,4 +1,11 @@
person.jenny:
entity_picture: "/local/avatars/jenny-bty.jpg"
person.florian:
entity_picture: "/local/avatars/flo-mtb.jpg"
entity_picture: "/local/avatars/flo-mtb.jpg"
device_tracker.sm_g985f:
entity_picture: "/local/avatars/jenny-bty.jpg"
icon: mdi:cellphone
device_tracker.pixely:
entity_picture: "/local/avatars/flo-mtb.jpg"
icon: mdi:cellphone

View File

@ -0,0 +1,63 @@
# https://console.actions.google.com/u/0/project/home-assistant-e12c3/overview
project_id: !secret google_project_id
service_account: !include google_assistant_service_keys.json
report_state: true
expose_by_default: false
exposed_domains:
- switch
- light
entity_config:
light.ambilight:
name: Ambilight
room: Wohnzimmer
light.kuchen_theke:
name: Küche
room: Küche
light.esstisch:
name: Esstisch
room: Wohnzimmer
light.office_rgb:
name: Büro
room: Büro
light.lichtleiste:
name: Schreibtischlicht
room: Büro
light.stimmungslicht:
name: Stimmungslicht
room: Wohnzimmer
light.lichterkette:
name: Lichterkette
room: Garten
light.weihnachtsbaum:
name: Weihnachtsbaum
room: Wohnzimmer
light.onair:
name: Studio-Treppe
room: Studio
switch.livingroom_music:
name: Musik
room: Wohnzimmer
switch.harmony_firetv:
name: Fernseher
room: Wohnzimmer
switch.harmony_steamlink:
name: Konsole
room: Wohnzimmer
switch.harmony_playstation:
name: Playstation
room: Wohnzimmer
switch.harmony_denon_power:
name: Receiver
room: Wohnzimmer
switch.desktop_wol:
name: Computer
room: Büro
switch.wallboard_display:
name: Display
room: Wohnzimmer
switch.osram_plug_01_57b6060a_on_off:
name: Ring
room: Studio
switch.onair_lamp_recording:
name: Aufnahme
room: Studio

View File

@ -1,7 +1,16 @@
- platform: hyperion
name: Ambilight
host: !secret ambilight_ip
hdmi_priority: 900
- platform: group
name: Küchen-Theke
entities:
- light.kitchen1
- light.kitchen2
- platform: group
name: Esstisch
entities:
- light.dining1
- light.dining2
# - platform: hyperion
# name: Ambilight
# host: !secret ambilight_ip
- platform: group
name: onAir
entities:
@ -9,7 +18,7 @@
- light.innr_gu10_rgb_2
- platform: switch
name: Lichterkette
entity_id: switch.innr_steckdose
entity_id: switch.garden_chain_of_lights
- platform: switch
name: Stimmungslicht
entity_id: switch.livingroom_stimmungslicht
entity_id: switch.moodlight

View File

@ -0,0 +1,54 @@
vacuum_living_room:
alias: "Wohnzimmer saugen"
sequence:
- service: vacuum.send_command
data:
entity_id: vacuum.dobby
command: spot_area
params:
rooms: 0
cleanings: 1
vacuum_kitchen:
alias: "Küche saugen"
sequence:
- service: vacuum.send_command
data:
entity_id: vacuum.dobby
command: spot_area
params:
rooms: 1
cleanings: 1
vacuum_corridor:
alias: "Flur saugen"
sequence:
- service: vacuum.send_command
data:
entity_id: vacuum.dobby
command: spot_area
params:
rooms: 2
cleanings: 1
vacuum_laundry:
alias: "HWR saugen"
sequence:
- service: vacuum.send_command
data:
entity_id: vacuum.dobby
command: spot_area
params:
rooms: 3
cleanings: 1
vacuum_dining_room:
alias: "Esszimmer saugen"
sequence:
- service: vacuum.send_command
data:
entity_id: vacuum.dobby
command: spot_area
params:
rooms: 4
cleanings: 1

View File

@ -1,5 +0,0 @@
- platform: template
sensors:
device_mobile_fb_battery:
value_template: '{{ states.device_tracker.mobile_fb.attributes.battery }}'
unit_of_measurement: '%'

View File

@ -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

View File

@ -14,6 +14,12 @@
count: 2
scan_interval: 15
- platform: ping
name: desktop_jenny_ping
host: !secret desktop_jenny_ip
count: 2
scan_interval: 15
- platform: template
sensors:
anyone_home:

View File

@ -1,6 +1,6 @@
- platform: command_line
name: Wallboard HDMI Status
command: !secret wallboard_hdmi_status_cmd
payload_on: display_power=1
payload_off: display_power=0
scan_interval: 60
# - platform: command_line
# name: Wallboard HDMI Status
# command: !secret wallboard_hdmi_status_cmd
# payload_on: display_power=1
# payload_off: display_power=0
# scan_interval: 60

View File

@ -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 %}

View File

@ -14,6 +14,19 @@
turn_off:
service: script.dummy
desktop_jenny_wol:
value_template: "{{ is_state('binary_sensor.desktop_jenny_ping', 'on') }}"
turn_on:
service: shell_command.ssh
data:
sshkey: !secret sshkey_wakeonlan
host: !secret nas_ip
user: !secret nas_ssh_user
port: !secret nas_ssh_port
command_or_param: !secret desktop_jenny_mac
turn_off:
service: script.dummy
onair_lamp_recording:
value_template: "{{ is_state('input_boolean.onair_lamp_recording', 'on') }}"
turn_on:

View File

@ -1,10 +1,9 @@
- platform: template
switches:
wallboard_display:
friendly_name: Wallboard Display Toggle
value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}"
turn_on:
service: script.wallboard_hdmi_on
turn_off:
service: script.wallboard_hdmi_off
#- platform: template
# switches:
# wallboard_display:
# friendly_name: Wallboard Display Toggle
# value_template: "{{ is_state('binary_sensor.wallboard_hdmi_status', 'on') }}"
# turn_on:
# service: script.wallboard_hdmi_on
# turn_off:
# service: script.wallboard_hdmi_off

View File

@ -1,20 +1,15 @@
icon: mdi:cellphone-link
path: devices
cards:
- type: entities
entities:
- entity: sensor.myip
name: Öffentliche IP
icon: mdi:earth
- type: horizontal-stack
cards:
- type: entities
title: Florian
show_header_toggle: false
entities:
- entity: device_tracker.pixel_4
- entity: device_tracker.pixely
name: Standort
- entity: sensor.battery_level
- entity: sensor.pixely_akkufullstand
name: Handy-Akku
icon: mdi:battery
- type: entities
@ -29,28 +24,23 @@ cards:
- type: horizontal-stack
cards:
- type: entities
title: Wake On Lan
title: WOL Florian
show_header_toggle: false
entities:
- entity: switch.desktop_wol
name: Desktop-PC
icon: mdi:desktop-classic
- type: entities
title: Wallboard
title: WOL Jenny
show_header_toggle: false
entities:
- entity: switch.wallboard_display
name: Display
icon: mdi:tablet
- entity: switch.desktop_jenny_wol
name: Desktop-PC Ronny
icon: mdi:desktop-classic
- type: entities
title: Home Assistant Slaves
title: Wallboard
show_header_toggle: false
entities:
- type: weblink
name: Livingroom
url: !secret url_haslave_livingroom
icon: mdi:home-assistant
- type: weblink
name: Office
url: !secret url_haslave_office
icon: mdi:home-assistant
- entity: switch.wallboard_display
name: Display
icon: mdi:tablet

View File

@ -1,6 +1,7 @@
#icon: mdi:home-variant-outline
title: Haus
path: floor
icon: 'mdi:floor-plan'
panel: true
cards:
- type: picture-elements

View File

@ -1,151 +1,103 @@
icon: mdi:water-percent
path: humidity
cards:
- type: vertical-stack
- type: grid
title: Wohnzimmer
columns: 2
square: true
cards:
- type: markdown
content: "### Elternbad"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.hygro_bathroom_parents_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.hygro_bathroom_parents_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: markdown
content: "### \"Kinderbad\""
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.hygro_bathroom_kids_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.hygro_bathroom_kids_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: markdown
content: "### Schlafzimmer"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.lumi_bedroom_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.lumi_bedroom_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: markdown
content: "### Gästezimmer"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.lumi_guestroom_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.lumi_guestroom_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: vertical-stack
- type: sensor
entity: sensor.lumi_livingroom_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 24
- type: sensor
entity: sensor.lumi_livingroom_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 24
- type: grid
title: Schlafzimmer
columns: 2
square: true
cards:
- type: markdown
content: "### Wohnzimmer"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.lumi_livingroom_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.lumi_livingroom_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: markdown
content: "### Hauswirtschaftsraum"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.hygro_hwr_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.hygro_hwr_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: markdown
content: "### Büro"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.lumi_office_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.lumi_office_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: markdown
content: "### Dachboden"
- type: horizontal-stack
cards:
- type: sensor
entity: sensor.attic_humidity_2
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.attic_temperature_2
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 12
- type: sensor
entity: sensor.lumi_bedroom_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 24
- type: sensor
entity: sensor.lumi_bedroom_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 24
- type: grid
title: Gästezimmer
columns: 2
square: true
cards:
- type: sensor
entity: sensor.lumi_guestroom_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 24
- type: sensor
entity: sensor.lumi_guestroom_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 24
- type: grid
title: Büro
columns: 2
square: true
cards:
- type: sensor
entity: sensor.lumi_office_humidity
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 24
- type: sensor
entity: sensor.lumi_office_temperature
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 24
- type: grid
title: Dachboden
columns: 2
square: true
cards:
- type: sensor
entity: sensor.attic_humidity_2
name: Luftfeuchtigkeit
graph: line
unit: "%"
detail: 2
hours_to_show: 24
- type: sensor
entity: sensor.attic_temperature_2
name: Temperatur
graph: line
unit: °C
detail: 2
hours_to_show: 24

View File

@ -1,24 +1,42 @@
title: Lichter
path: lights
icon: 'mdi:lightbulb'
cards:
- type: light
entity: light.kuchen_theke
name: Küchen-Theke
- type: light
entity: light.stimmungslicht
name: Stimmungslicht
- type: light
entity: light.ambilight
name: Ambilight
- type: light
entity: light.lichterkette
name: Lichterkette (Garten)
- type: light
entity: light.esstisch
name: Esstisch
- type: light
entity: light.tint_rgb_gu10_1
name: Kinderbad
- type: grid
title: Erdgeschoss
columns: 3
suqare: true
cards:
- type: light
entity: light.kuchen_theke
name: Küchen-Theke
- type: light
entity: light.esstisch
name: Esstisch
- type: light
entity: light.stimmungslicht
name: Stimmungslicht
- type: light
entity: light.ambilight
name: Ambilight
- type: light
entity: light.lichterkette
name: Garten
- type: grid
title: 1. Stock
columns: 3
suqare: true
cards:
- type: light
entity: light.office_rgb
name: Bürolicht
- type: light
entity: light.lichtleiste
name: Schreibtischlicht
- type: light
entity: light.tint_rgb_gu10_1
name: Kinderbad
- type: vertical-stack
title: Studio
@ -29,19 +47,11 @@ cards:
- type: entities
show_header_toggle: false
entities:
- entity: light.philips_iris
name: Iris
- entity: switch.onair_lamp_recording
name: Studio Aufnahme
icon: mdi:camera-rear
- entity: switch.osram_plug_01_57b6060a_on_off
name: Studio Ringlicht
icon: mdi:checkbox-blank-circle-outline
- type: vertical-stack
title: Büro
cards:
- type: light
entity: light.office_rgb
name: Deckenlicht
- type: light
entity: light.lichtleiste
name: Schreibtischlicht

View File

@ -1,20 +1,9 @@
title: Wohnzimmer
path: livingroom
title: Media
path: media
icon: 'mdi:theater'
cards:
# Lights
- type: horizontal-stack
cards:
- type: entities
entities:
- entity: light.kuchen_theke
name: Küchen-Theke
- entity: light.esstisch
name: Esstisch
- entity: light.stimmungslicht
name: Stimmungslicht
icon: mdi:lightbulb
- entity: light.ambilight
name: Ambilight
- type: media-control
entity: media_player.spotify
# Denon + Harmony
- type: vertical-stack
@ -45,14 +34,11 @@ cards:
icon: mdi:bluetooth-audio
- entity: switch.harmony_steamlink
icon: mdi:steam
- entity: switch.harmony_playstation
icon: mdi:playstation
state_image:
"PowerOff": /local/images/power.jpg
"Fire TV sehen": /local/images/firetv.jpg
"Musik Bluetooth": /local/images/music.jpg
"SteamLink": /local/images/steamlink.jpg
"PlayStation": /local/images/playstation.jpg
entity: sensor.harmony_activity
# Spotify

View File

@ -3,6 +3,10 @@ path: network
cards:
- type: vertical-stack
cards:
- type: entity
entity: sensor.myip
name: Öffentliche IP
icon: mdi:earth
- type: gauge
entity: sensor.adguard_average_processing_speed
max: 100

View File

@ -1,5 +1,6 @@
title: Übersicht
path: overview
icon: "mdi:tablet-dashboard"
cards:
- type: conditional
conditions:
@ -16,11 +17,12 @@ cards:
- type: vertical-stack
cards:
- type: custom:weather-card
entity: weather.openweathermap
name: Wetter
- type: iframe
url: !secret iframe_windy
aspect_ratio: 75%
entity: weather.openweathermap
icons: "/local/lovelace/custom/weather-card/icons/"
# - type: iframe
# url: !secret iframe_windy
# aspect_ratio: 75%
# - type: iframe
# url: !secret iframe_earth
# aspect_ratio: 75%
@ -45,7 +47,7 @@ cards:
- person.florian
- person.jenny
- type: map
aspect_ratio: 75%
default_zoom: 15
entities:
- zone.home
aspect_ratio: 60%
entities:
- device_tracker.sm_g985f
- device_tracker.pixel_4

View File

@ -1,8 +1,48 @@
title: Mäheroboter
path: mower
title: Roboter
path: robots
badges: []
icon: 'mdi:robot-vacuum-variant'
cards:
# Dobby
- type: 'custom:vacuum-card'
entity: vacuum.dobby
map: camera.deebot_dobby_live_map
stats:
default:
- entity_id: sensor.dobby_heap
unit: Stunden
subtitle: Filter
- entity_id: sensor.dobby_sidebrush
unit: Stunden
subtitle: Seitenbürste
- entity_id: sensor.dobby_brush
unit: Stunden
subtitle: Hauptbürste
cleaning:
- entity_id: sensor.dobby_stats_area
unit: m2
subtitle: Gereinigter Bereich
- entity_id: sensor.dobby_stats_time
unit: Minuten
subtitle: Dauer
actions:
- name: Wohnzimmer
service: script.vacuum_living_room
icon: 'mdi:sofa'
- name: Küche
service: script.vacuum_kitchen
icon: 'mdi:pot-steam'
- name: Esszimmer
service: script.vacuum_dining_room
icon: 'mdi:table-furniture'
- name: HWR
service: script.vacuum_laundry
icon: 'mdi:washing-machine'
- name: Flur
service: script.vacuum_corridor
icon: 'mdi:door'
# Hans-Dieter
- type: vertical-stack
cards:
- elements:

View File

@ -1,5 +0,0 @@
icon: mdi:spotify
path: spotify
cards:
- type: media-control
entity: media_player.spotify

View File

@ -1,5 +1,7 @@
homeassistant:
name: Home
internal_url: !secret url_internal
external_url: !secret url_external
latitude: !secret home_lat
longitude: !secret home_long
elevation: !secret home_elevation
@ -15,12 +17,11 @@ homeassistant:
- 192.168.0.0/16
- fd00::/8
# landroid
packages: !include_dir_named packages
#packages: !include_dir_named packages
config:
conversation:
device_tracker:
#discovery:
dhcp:
history:
logbook:
map:
@ -35,31 +36,36 @@ http:
ip_ban_enabled: true
login_attempts_threshold: 5
use_x_forwarded_for: true
base_url: !secret url_base
trusted_proxies:
- 127.0.0.1
- ::1
# traefik.web
- 172.19.0.0/24
frontend:
themes: !include_dir_merge_named themes/
lovelace:
mode: yaml
resources:
- url: /local/lovelace/custom/weather-card/weather-card.js
type: module
- url: /local/lovelace/custom/vacuum-card/vacuum-card.js
type: module
- url: /local/lovelace/custom/auto-entities/auto-entities.js
type: module
- url: /local/lovelace/custom/color-lite-card/color-lite-card.js
type: module
- url: /local/lovelace/custom/now-playing-card/now-playing-card.js
type: module
tts:
- platform: google_translate
service_name: google_say
zha:
usb_path: /dev/ttyACM0
radio_type: deconz
database_path: /config/zigbee.db
emulated_roku:
servers:
- name: Home Assistant
listen_port: !secret roku_port
mqtt:
broker: !secret mqtt_broker_ip
username: !secret mqtt_username
@ -90,32 +96,18 @@ notify:
- platform: command_line
name: alexa_all
command: "/config/alexa_wrapper.sh -d 'ALL'"
# - name: android
# platform: fcm-android
media_player:
- platform: androidtv
device_class: firetv
name: Fire TV
host: !secret firetv_ip
adbkey: !secret adbkey
get_sources: true
remote:
- platform: harmony
name: Livingroom Harmony
host: !secret harmonyhub_ip
delay_secs: 0.3
person:
- name: Jenny
id: jenny
user_id: !secret userId_jenny
device_trackers:
- device_tracker.sm_g985f
- name: Florian
id: florian
user_id: !secret userId_florian
device_trackers:
- device_tracker.pixel_4
- device_tracker.pixely
zone:
- name: !secret work_name_f
@ -129,10 +121,6 @@ zone:
radius: 500
icon: mdi:briefcase
weather:
- platform: openweathermap
api_key: !secret openweathermap
ffmpeg:
ffmpeg_bin: /usr/bin/ffmpeg
@ -198,20 +186,27 @@ spotify:
client_id: !secret spotify_client_id
client_secret: !secret spotify_client_secret
ecovacs:
deebot:
username: !secret ecovacs_user
password: !secret ecovacs_password
country: de
continent: eu
deviceid:
- !secret ecovacs_serial_dobby
live_map: true
show_color_rooms: true
livemappath: 'www/vacuums/'
#tplink:
# discovery: false
# switch:
# - host: !secret tplink_ip
fontawesome:
regular:
solid:
brands:
# External config files
alert: !include_dir_merge_named config/alerts/
alexa: !include config/alexa.yaml
google_assistant: !include config/google_assistant.yaml
alert: !include_dir_merge_named config/alerts/
automation: !include_dir_merge_list config/automations/
binary_sensor: !include_dir_merge_list config/sensors_binary/
camera: !include config/cameras.yaml

View File

@ -0,0 +1,119 @@
"""Support for Deebot Vaccums."""
import asyncio
import logging
import async_timeout
import time
import random
import string
import base64
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from datetime import timedelta
from deebotozmo import *
from homeassistant.util import Throttle
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['deebotozmo==1.7.8']
CONF_COUNTRY = "country"
CONF_CONTINENT = "continent"
CONF_DEVICEID = "deviceid"
CONF_LIVEMAPPATH = "livemappath"
CONF_LIVEMAP = "live_map"
CONF_SHOWCOLORROOMS = "show_color_rooms"
DEEBOT_DEVICES = "deebot_devices"
# Generate a random device ID on each bootup
DEEBOT_API_DEVICEID = "".join(
random.choice(string.ascii_uppercase + string.digits) for _ in range(8)
)
_LOGGER = logging.getLogger(__name__)
HUB = None
DOMAIN = 'deebot'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): vol.All(vol.Lower, cv.string),
vol.Required(CONF_CONTINENT): vol.All(vol.Lower, cv.string),
vol.Required(CONF_DEVICEID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_LIVEMAP, default=True): cv.boolean,
vol.Optional(CONF_SHOWCOLORROOMS, default=False): cv.boolean,
vol.Optional(CONF_LIVEMAPPATH, default='www/'): cv.string
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Deebot."""
global HUB
HUB = DeebotHub(config[DOMAIN])
for component in ('sensor', 'binary_sensor', 'vacuum'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
class DeebotHub(Entity):
"""Deebot Hub"""
def __init__(self, domain_config):
"""Initialize the Deebot Vacuum."""
self.config = domain_config
self._lock = threading.Lock()
self.ecovacs_api = EcoVacsAPI(
DEEBOT_API_DEVICEID,
domain_config.get(CONF_USERNAME),
EcoVacsAPI.md5(domain_config.get(CONF_PASSWORD)),
domain_config.get(CONF_COUNTRY),
domain_config.get(CONF_CONTINENT)
)
devices = self.ecovacs_api.devices()
liveMapEnabled = domain_config.get(CONF_LIVEMAP)
liveMapRooms = domain_config.get(CONF_SHOWCOLORROOMS)
country = domain_config.get(CONF_COUNTRY).lower()
continent = domain_config.get(CONF_CONTINENT).lower()
self.vacbots = []
# CREATE VACBOT FOR EACH DEVICE
for device in devices:
if device['name'] in domain_config.get(CONF_DEVICEID):
vacbot = VacBot(
self.ecovacs_api.uid,
self.ecovacs_api.resource,
self.ecovacs_api.user_access_token,
device,
country,
continent,
liveMapEnabled,
liveMapRooms
)
_LOGGER.debug("New vacbot found: " + device['name'])
self.vacbots.append(vacbot)
_LOGGER.debug("Hub initialized")
@Throttle(timedelta(seconds=10))
def update(self):
""" Update all statuses. """
try:
for vacbot in self.vacbots:
vacbot.request_all_statuses()
except Exception as ex:
_LOGGER.error('Update failed: %s', ex)
raise
@property
def name(self):
""" Return the name of the hub."""
return "Deebot Hub"

View File

@ -0,0 +1,48 @@
"""Support for Deebot Sensor."""
from typing import Optional
from deebotozmo import *
from homeassistant.components.binary_sensor import BinarySensorEntity
from . import HUB as hub
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deebot binary sensor platform."""
hub.update()
for vacbot in hub.vacbots:
add_devices([DeebotMopAttachedBinarySensor(vacbot, "mop_attached")], True)
class DeebotMopAttachedBinarySensor(BinarySensorEntity):
"""Deebot mop attached binary sensor"""
def __init__(self, vacbot: VacBot, device_id: str):
"""Initialize the Sensor."""
self._vacbot = vacbot
self._id = device_id
if self._vacbot.vacuum.get("nick", None) is not None:
self._vacbot_name = "{}".format(self._vacbot.vacuum["nick"])
else:
# In case there is no nickname defined, use the device id
self._vacbot_name = "{}".format(self._vacbot.vacuum["did"])
self._name = self._vacbot_name + "_" + device_id
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def is_on(self):
return self._vacbot.mop_attached
@property
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
return "mdi:water" if self.is_on else "mdi:water-off"

View File

@ -0,0 +1,11 @@
{
"domain": "deebot",
"name": "Deebot for Hassio",
"documentation": "https://github.com/And3rsL/Deebot-for-hassio",
"requirements": [
"deebotozmo==1.7.8"
],
"dependencies": [],
"codeowners": ["@And3rsL"],
"homeassistant": "0.110.0"
}

View File

@ -0,0 +1,179 @@
"""Support for Deebot Sensor."""
from typing import Optional
from deebotozmo import *
from homeassistant.const import (STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity
from . import HUB as hub
_LOGGER = logging.getLogger(__name__)
from homeassistant.components.vacuum import (
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
)
STATE_CODE_TO_STATE = {
'STATE_IDLE': STATE_IDLE,
'STATE_CLEANING': STATE_CLEANING,
'STATE_RETURNING': STATE_RETURNING,
'STATE_DOCKED': STATE_DOCKED,
'STATE_ERROR': STATE_ERROR,
'STATE_PAUSED': STATE_PAUSED,
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deebot sensor."""
hub.update()
for vacbot in hub.vacbots:
# General
add_devices([DeebotLastCleanImageSensor(vacbot, "last_clean_image")], True)
add_devices([DeebotWaterLevelSensor(vacbot, "water_level")], True)
# Components
add_devices([DeebotComponentSensor(vacbot, COMPONENT_MAIN_BRUSH)], True)
add_devices([DeebotComponentSensor(vacbot, COMPONENT_SIDE_BRUSH)], True)
add_devices([DeebotComponentSensor(vacbot, COMPONENT_FILTER)], True)
# Stats
add_devices([DeebotStatsSensor(vacbot, "stats_area")], True)
add_devices([DeebotStatsSensor(vacbot, "stats_time")], True)
add_devices([DeebotStatsSensor(vacbot, "stats_type")], True)
class DeebotBaseSensor(Entity):
"""Deebot base sensor"""
def __init__(self, vacbot, device_id):
"""Initialize the Sensor."""
self._state = STATE_UNKNOWN
self._vacbot = vacbot
self._id = device_id
if self._vacbot.vacuum.get("nick", None) is not None:
self._vacbot_name = "{}".format(self._vacbot.vacuum["nick"])
else:
# In case there is no nickname defined, use the device id
self._vacbot_name = "{}".format(self._vacbot.vacuum["did"])
self._name = self._vacbot_name + "_" + device_id
@property
def name(self):
"""Return the name of the device."""
return self._name
class DeebotLastCleanImageSensor(DeebotBaseSensor):
"""Deebot Sensor"""
def __init__(self, vacbot, device_id):
"""Initialize the Sensor."""
super(DeebotLastCleanImageSensor, self).__init__(vacbot, device_id)
@property
def state(self):
"""Return the state of the vacuum cleaner."""
if self._vacbot.last_clean_image is not None:
return self._vacbot.last_clean_image
@property
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
return "mdi:image-search"
class DeebotWaterLevelSensor(DeebotBaseSensor):
"""Deebot Sensor"""
def __init__(self, vacbot, device_id):
"""Initialize the Sensor."""
super(DeebotWaterLevelSensor, self).__init__(vacbot, device_id)
@property
def state(self):
"""Return the state of the vacuum cleaner."""
if self._vacbot.water_level is not None:
return self._vacbot.water_level
@property
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
return "mdi:water"
class DeebotComponentSensor(DeebotBaseSensor):
"""Deebot Sensor"""
def __init__(self, vacbot, device_id):
"""Initialize the Sensor."""
super(DeebotComponentSensor, self).__init__(vacbot, device_id)
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return '%'
@property
def state(self):
"""Return the state of the vacuum cleaner."""
for key, val in self._vacbot.components.items():
if key == self._id:
return int(val)
@property
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
if self._id == COMPONENT_MAIN_BRUSH or self._id == COMPONENT_SIDE_BRUSH:
return "mdi:broom"
elif self._id == COMPONENT_FILTER:
return "mdi:air-filter"
class DeebotStatsSensor(DeebotBaseSensor):
"""Deebot Sensor"""
def __init__(self, vacbot, device_id):
"""Initialize the Sensor."""
super(DeebotStatsSensor, self).__init__(vacbot, device_id)
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
if self._id == 'stats_area':
return "mq"
elif self._id == 'stats_time':
return "min"
@property
def state(self):
"""Return the state of the vacuum cleaner."""
if self._id == 'stats_area' and self._vacbot.stats_area is not None:
return int(self._vacbot.stats_area)
elif self._id == 'stats_time' and self._vacbot.stats_time is not None:
return int(self._vacbot.stats_time/60)
elif self._id == 'stats_type':
return self._vacbot.stats_type
else:
return STATE_UNKNOWN
@property
def icon(self) -> Optional[str]:
"""Return the icon to use in the frontend, if any."""
if self._id == 'stats_area':
return "mdi:floor-plan"
elif self._id == 'stats_time':
return "mdi:timer-outline"
elif self._id == 'stats_type':
return "mdi:cog"

View File

@ -0,0 +1,247 @@
"""Support for Deebot Vaccums."""
import base64
from typing import Optional, Dict, Any, Union, List
from deebotozmo import *
from homeassistant.util import slugify
from . import HUB as hub
CONF_COUNTRY = "country"
CONF_CONTINENT = "continent"
CONF_DEVICEID = "deviceid"
CONF_LIVEMAPPATH = "livemappath"
CONF_LIVEMAP = "live_map"
CONF_SHOWCOLORROOMS = "show_color_rooms"
DEEBOT_DEVICES = "deebot_devices"
from homeassistant.components.vacuum import (
PLATFORM_SCHEMA,
STATE_CLEANING,
STATE_DOCKED,
STATE_ERROR,
STATE_IDLE,
STATE_PAUSED,
STATE_RETURNING,
SUPPORT_BATTERY,
SUPPORT_FAN_SPEED,
SUPPORT_LOCATE,
SUPPORT_PAUSE,
SUPPORT_RETURN_HOME,
SUPPORT_SEND_COMMAND,
SUPPORT_START,
SUPPORT_STATE,
VacuumEntity,
)
_LOGGER = logging.getLogger(__name__)
SUPPORT_DEEBOT = (
SUPPORT_BATTERY
| SUPPORT_FAN_SPEED
| SUPPORT_LOCATE
| SUPPORT_PAUSE
| SUPPORT_RETURN_HOME
| SUPPORT_SEND_COMMAND
| SUPPORT_START
| SUPPORT_STATE
)
STATE_CODE_TO_STATE = {
'STATE_IDLE': STATE_IDLE,
'STATE_CLEANING': STATE_CLEANING,
'STATE_RETURNING': STATE_RETURNING,
'STATE_DOCKED': STATE_DOCKED,
'STATE_ERROR': STATE_ERROR,
'STATE_PAUSED': STATE_PAUSED,
}
ATTR_COMPONENT_PREFIX = "component_"
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deebot vacuums."""
if DEEBOT_DEVICES not in hass.data:
hass.data[DEEBOT_DEVICES] = []
for vacbot in hub.vacbots:
vacuum = DeebotVacuum(hass, vacbot)
add_devices([vacuum])
class DeebotVacuum(VacuumEntity):
"""Deebot Vacuums"""
def __init__(self, hass, vacbot):
"""Initialize the Deebot Vacuum."""
self._hass = hass
self.device = vacbot
if self.device.vacuum.get("nick", None) is not None:
self._name = "{}".format(self.device.vacuum["nick"])
else:
# In case there is no nickname defined, use the device id
self._name = "{}".format(self.device.vacuum["did"])
self._fan_speed = None
self._live_map = None
self._live_map_path = hub.config.get(CONF_LIVEMAPPATH) + self._name + '_liveMap.png'
self.device.refresh_statuses()
_LOGGER.debug("Vacuum initialized: %s", self.name)
def on_fan_change(self, fan_speed):
self._fan_speed = fan_speed
@property
def should_poll(self) -> bool:
"""Return True if entity has to be polled for state."""
return True
@property
def unique_id(self) -> str:
"""Return an unique ID."""
return self.device.vacuum.get("did", None)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def supported_features(self):
"""Flag vacuum cleaner robot features that are supported."""
return SUPPORT_DEEBOT
@property
def state(self):
"""Return the state of the vacuum cleaner."""
if self.device.vacuum_status is not None and self.device.is_available == True:
return STATE_CODE_TO_STATE[self.device.vacuum_status]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self.device.is_available
async def async_return_to_base(self, **kwargs):
"""Set the vacuum cleaner to return to the dock."""
await self.hass.async_add_executor_job(self.device.Charge)
@property
def battery_level(self):
"""Return the battery level of the vacuum cleaner."""
if self.device.battery_status is not None:
return self.device.battery_status
return super().battery_level
@property
def fan_speed(self):
"""Return the fan speed of the vacuum cleaner."""
return self.device.fan_speed
async def async_set_fan_speed(self, fan_speed, **kwargs):
await self.hass.async_add_executor_job(self.device.SetFanSpeed, fan_speed)
@property
def fan_speed_list(self):
"""Get the list of available fan speed steps of the vacuum cleaner."""
return [FAN_SPEED_QUIET, FAN_SPEED_NORMAL, FAN_SPEED_MAX, FAN_SPEED_MAXPLUS]
async def async_pause(self):
"""Pause the vacuum cleaner."""
await self.hass.async_add_executor_job(self.device.CleanPause)
async def async_start(self):
"""Start the vacuum cleaner."""
await self.hass.async_add_executor_job(self.device.CleanResume)
async def async_locate(self, **kwargs):
"""Locate the vacuum cleaner."""
await self.hass.async_add_executor_job(self.device.PlaySound)
async def async_send_command(self, command, params=None, **kwargs):
"""Send a command to a vacuum cleaner."""
_LOGGER.debug("async_send_command %s (%s), %s", command, params, kwargs)
if command == 'spot_area':
await self.hass.async_add_executor_job(self.device.SpotArea, params['rooms'], params['cleanings'])
return
if command == 'custom_area':
await self.hass.async_add_executor_job(self.device.CustomArea, params['coordinates'], params['cleanings'])
return
if command == 'set_water':
await self.hass.async_add_executor_job(self.device.SetWaterLevel, params['amount'])
return
if command == 'relocate':
await self.hass.async_add_executor_job(self.device.Relocate)
return
if command == 'auto_clean':
self.hass.async_add_executor_job(self.device.Clean, params['type'])
return
if command == 'refresh_components':
await self.hass.async_add_executor_job(self.device.refresh_components)
return
if command == 'refresh_statuses':
await self.hass.async_add_executor_job(self.device.refresh_statuses)
return
if command == 'refresh_live_map':
await self.hass.async_add_executor_job(self.device.refresh_liveMap)
return
if command == 'save_live_map':
if(self._live_map != self.device.live_map):
self._live_map = self.device.live_map
with open(params['path'], "wb") as fh:
fh.write(base64.decodebytes(self.device.live_map))
await self.hass.async_add_executor_job(self.device.exc_command, command, params)
async def async_update(self):
"""Fetch state from the device."""
await self.hass.async_add_executor_job(self.device.request_all_statuses)
try:
if(self._live_map != self.device.live_map):
self._live_map = self.device.live_map
with open(self._live_map_path, "wb") as fh:
fh.write(base64.decodebytes(self.device.live_map))
except KeyError:
_LOGGER.warning("Can't access local folder: %s", self._live_map_path)
@property
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
"""Return device specific state attributes.
Implemented by platform classes. Convention for attribute names
is lowercase snake_case.
"""
data: Dict[str, Union[int, List[int]]] = {}
# Needed for custom vacuum-card (https://github.com/denysdovhan/vacuum-card)
# Should find a better way without breaking everyone rooms script
data['status'] = STATE_CODE_TO_STATE[self.device.vacuum_status]
if self.device.getSavedRooms() is not None:
for r in self.device.getSavedRooms():
# convert room name to snake_case to meet the convention
room_name = "room_" + slugify(r["subtype"])
room_values = data.get(room_name)
if room_values is None:
data[room_name] = r["id"]
elif isinstance(room_values, list):
room_values.append(r["id"])
else:
# Convert from int to list
data[room_name] = [room_values, r["id"]]
return data

View File

@ -0,0 +1,50 @@
DOMAIN = "fontawesome"
DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url'
ICONS_URL = f'/{DOMAIN}/'
ICON_FILES = {
'regular': 'far.js',
'solid': 'fas.js',
'brands': 'fab.js',
}
async def async_setup(hass, config):
for f in ICON_FILES.values():
hass.http.register_static_path(
f"/{DOMAIN}/{f}",
hass.config.path(f"custom_components/{DOMAIN}/data/{f}"),
True
)
conf = config.get(DOMAIN)
if not conf:
return True
register_modules(hass, conf)
return True
async def async_setup_entry(hass, config_entry):
config_entry.add_update_listener(_update_listener)
register_modules(hass, config_entry.options)
return True
async def async_remove_entry(hass, config_entry):
register_modules(hass, [])
return True
async def _update_listener(hass, config_entry):
register_modules(hass, config_entry.options)
return True
def register_modules(hass, modules):
if DATA_EXTRA_MODULE_URL not in hass.data:
hass.data[DATA_EXTRA_MODULE_URL] = set()
url_set = hass.data[DATA_EXTRA_MODULE_URL]
for k, v in ICON_FILES.items():
url_set.discard(ICONS_URL+v)
if k in modules and modules[k] is not False:
url_set.add(ICONS_URL+v)

View File

@ -0,0 +1,49 @@
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
_LOGGER = logging.getLogger(__name__)
@config_entries.HANDLERS.register("fontawesome")
class FontawesomeConfigFlow(config_entries.ConfigFlow):
async def async_step_user(self, user_input=None):
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="", data={})
@staticmethod
@callback
def async_get_options_flow(config_entry):
return FontawesomeEditFlow(config_entry)
class FontawesomeEditFlow(config_entries.OptionsFlow):
def __init__(self, config_entry):
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
"regular",
default=self.config_entry.options.get("regular", False),
): bool,
vol.Optional(
"solid",
default=self.config_entry.options.get("solid", False),
): bool,
vol.Optional(
"brands",
default=self.config_entry.options.get("brands", False),
): bool,
}
)
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
{
"domain": "fontawesome",
"name": "Fontawesome icons",
"documentation": "",
"dependencies": ["frontend"],
"codeowners": [],
"requirements": [],
"config_flow": true
}

View File

@ -0,0 +1,21 @@
{
"config": {
"title": "FontAwesome",
"abort": {
"single_instance_allowed": "Only a single configuration of FontAwesome is allowed."
}
},
"options": {
"step": {
"init": {
"title": "Icon sets",
"description": "Which icon sets to include",
"data": {
"regular": "Include Regular icons (far:)",
"solid": "Include Solid icons (fas:)",
"brands": "Include Brand icons (fab:)"
}
}
}
}
}

View File

@ -57,11 +57,11 @@ API_WORX_SENSORS = {
},
"icon": "mdi:battery",
"unit": "%",
"device_class": None,
"device_class": "battery",
},
"error": {
"state": {"error_description": "state", "error": "error_id"},
"icon": None,
"icon": "mdi:alert",
"unit": None,
"device_class": None,
},
@ -83,6 +83,8 @@ API_WORX_SENSORS = {
"rain_delay": "raindelay",
"schedule_variation": "timeextension",
"firmware": "firmware_version",
"serial": "serial",
"mac": "mac",
},
"icon": None,
"unit": None,
@ -130,7 +132,7 @@ async def async_setup(hass, config):
async def handle_start(call):
"""Handle start service call."""
if "id" in call.data:
ID = call.data["id"]
ID = int(call.data["id"])
for cli in client:
attrs = vars(cli)
@ -144,7 +146,7 @@ async def async_setup(hass, config):
async def handle_pause(call):
"""Handle pause service call."""
if "id" in call.data:
ID = call.data["id"]
ID = int(call.data["id"])
for cli in client:
attrs = vars(cli)
@ -158,7 +160,7 @@ async def async_setup(hass, config):
async def handle_home(call):
"""Handle pause service call."""
if "id" in call.data:
ID = call.data["id"]
ID = int(call.data["id"])
for cli in client:
attrs = vars(cli)
@ -177,31 +179,46 @@ async def async_setup(hass, config):
if "id" in call.data:
_LOGGER.debug("Data from Home Assistant: %s", call.data["id"])
for cli in client:
attrs = vars(cli)
if (attrs["id"] == call.data["id"]):
if (attrs["id"] == int(call.data["id"])):
break
else:
id += 1
if "raindelay" in call.data:
tmpdata["rd"] = call.data["raindelay"]
tmpdata["rd"] = int(call.data["raindelay"])
_LOGGER.debug("Setting rain_delay for %s to %s", client[id].name, call.data["raindelay"])
sendData = True
if "timeextension" in call.data:
tmpdata["sc"] = {}
tmpdata["sc"]["p"] = call.data["timeextension"]
tmpdata["sc"] = {}
tmpdata["sc"]["p"] = int(call.data["timeextension"])
data = json.dumps(tmpdata)
_LOGGER.debug("Setting time_extension for %s to %s", client[id].name, call.data["timeextension"])
sendData = True
if "multizone_distances" in call.data:
tmpdata["mz"] = [int(x) for x in call.data["multizone_distances"]]
data = json.dumps(tmpdata)
_LOGGER.debug("Setting multizone distances for %s to %s", client[id].name, call.data["multizone_distances"])
sendData = True
if "multizone_probabilities" in call.data:
tmpdata["mzv"] = []
for idx, val in enumerate(call.data["multizone_probabilities"]):
for _ in range(val):
tmpdata["mzv"].append(idx)
data = json.dumps(tmpdata)
_LOGGER.debug("Setting multizone probabilities for %s to %s", client[id].name, call.data["multizone_probabilities"])
sendData = True
if sendData:
data = json.dumps(tmpdata)
_LOGGER.debug("Sending: %s", data)
client[id].sendData(data)
hass.services.async_register(DOMAIN, SERVICE_CONFIG, handle_config)
return True

View File

@ -1,7 +1,9 @@
{
"domain": "landroid_cloud",
"name": "Worx Landroid Cloud",
"documentation": "https://www.home-assistant.io/integrations/landroid_cloud/",
"requirements": ["pyworxcloud==1.2.17"],
"documentation": "https://github.com/MTrab/landroid_cloud/blob/master/README.md",
"issue_tracker": "https://github.com/MTrab/landroid_cloud/issues",
"requirements": ["pyworxcloud==1.2.21"],
"version": "1.6.5",
"codeowners": ["@MTrab"]
}

View File

@ -1,127 +1,132 @@
"""Support for monitoring Worx Landroid Sensors."""
import async_timeout
import asyncio
import logging
from homeassistant.components import sensor
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL
_LOGGER = logging.getLogger(__name__)
STATE_INITIALIZING = "Initializing"
STATE_OFFLINE = "Offline"
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the available sensors for Worx Landroid."""
if discovery_info is None:
return
entities = []
info = discovery_info[0]
for tSensor in API_WORX_SENSORS:
name = "{}_{}".format(info["name"].lower(), tSensor.lower())
friendly_name = "{} {}".format(info["friendly"], tSensor)
dev_id = info["id"]
api = hass.data[LANDROID_API][dev_id]
sensor_type = tSensor
_LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"])
entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id)
entities.append(entity)
async_add_entities(entities, True)
class LandroidSensor(Entity):
"""Class to create and populate a Landroid Sensor."""
def __init__(self, api, name, sensor_type, friendly_name, dev_id):
"""Init new sensor."""
self._api = api
self._attributes = {}
self._available = False
self._name = friendly_name
self._state = STATE_INITIALIZING
self._sensor_type = sensor_type
self._dev_id = dev_id
self.entity_id = sensor.ENTITY_ID_FORMAT.format(name)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
@property
def device_state_attributes(self):
"""Return sensor attributes."""
return self._attributes
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return API_WORX_SENSORS[self._sensor_type]["unit"]
@property
def icon(self):
"""Icon to use in the frontend."""
return API_WORX_SENSORS[self._sensor_type]["icon"]
@property
def should_poll(self):
"""Return False as entity is updated from the component."""
return False
@property
def state(self):
"""Return sensor state."""
return self._state
@callback
def update_callback(self):
"""Get new data and update state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Connect update callbacks."""
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback)
def _get_data(self):
"""Return new data from the api cache."""
data = self._api.get_data(self._sensor_type)
self._available = True
return data
async def async_update(self):
"""Update the sensor."""
_LOGGER.debug("Updating %s", self.entity_id)
data = self._get_data()
if "state" in data:
_LOGGER.debug(data)
state = data.pop("state")
_LOGGER.debug("Mower %s State %s", self._name, state)
self._attributes.update(data)
self._state = state
else:
_LOGGER.debug("No data received for %s", self.entity_id)
reachable = self._api._client.online
if not reachable:
if "_battery" in self.entity_id:
self._state = "Unknown"
else:
self._state = STATE_OFFLINE
#else:
# attrs = vars(self._api._client)
# for item in attrs:
# _LOGGER.debug("%s : %s", item, attrs[item])
"""Support for monitoring Worx Landroid Sensors."""
import async_timeout
import asyncio
import logging
from homeassistant.components import sensor
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
from . import API_WORX_SENSORS, LANDROID_API, UPDATE_SIGNAL
_LOGGER = logging.getLogger(__name__)
STATE_INITIALIZING = "Initializing"
STATE_OFFLINE = "Offline"
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the available sensors for Worx Landroid."""
if discovery_info is None:
return
entities = []
info = discovery_info[0]
for tSensor in API_WORX_SENSORS:
name = "{}_{}".format(info["name"].lower(), tSensor.lower())
friendly_name = "{} {}".format(info["friendly"], tSensor)
dev_id = info["id"]
api = hass.data[LANDROID_API][dev_id]
sensor_type = tSensor
_LOGGER.debug("Init Landroid %s sensor for %s", sensor_type, info["friendly"])
entity = LandroidSensor(api, name, sensor_type, friendly_name, dev_id)
entities.append(entity)
async_add_entities(entities, True)
class LandroidSensor(Entity):
"""Class to create and populate a Landroid Sensor."""
def __init__(self, api, name, sensor_type, friendly_name, dev_id):
"""Init new sensor."""
self._api = api
self._attributes = {}
self._available = False
self._name = friendly_name
self._state = STATE_INITIALIZING
self._sensor_type = sensor_type
self._dev_id = dev_id
self.entity_id = sensor.ENTITY_ID_FORMAT.format(name)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
@property
def device_state_attributes(self):
"""Return sensor attributes."""
return self._attributes
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return API_WORX_SENSORS[self._sensor_type]["unit"]
@property
def icon(self):
"""Icon to use in the frontend."""
if self._sensor_type == "battery" and isinstance(self.state, int):
charging = self._attributes["charging"]
return icon_for_battery_level(battery_level=self.state, charging=charging)
return API_WORX_SENSORS[self._sensor_type]["icon"]
@property
def should_poll(self):
"""Return False as entity is updated from the component."""
return False
@property
def state(self):
"""Return sensor state."""
return self._state
@callback
def update_callback(self):
"""Get new data and update state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Connect update callbacks."""
async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self.update_callback)
def _get_data(self):
"""Return new data from the api cache."""
data = self._api.get_data(self._sensor_type)
self._available = True
return data
async def async_update(self):
"""Update the sensor."""
_LOGGER.debug("Updating %s", self.entity_id)
data = self._get_data()
if "state" in data:
_LOGGER.debug(data)
state = data.pop("state")
_LOGGER.debug("Mower %s State %s", self._name, state)
self._attributes.update(data)
self._state = state
if "latitude" in self._attributes:
if self._attributes["latitude"] == None:
del self._attributes["latitude"]
del self._attributes["longitude"]
else:
_LOGGER.debug("No data received for %s", self.entity_id)
reachable = self._api._client.online
if not reachable:
if "_battery" in self.entity_id:
self._state = STATE_UNKNOWN
else:
self._state = STATE_OFFLINE

View File

@ -28,3 +28,9 @@ config:
timeextension:
description: Set time extension. Extension in % ranging from -100 to 100
example: -23
multizone_distances:
description: Set multizone distances. Distances in meter. 0 = Disabled
example: '[15, 80, 120, 155]'
multizone_probabilities:
description: Set multizone probabilities. Probabilities in parts-of-ten. 1 = 10%, 2 = 20%, ...
example: '[5, 1, 2, 2]'

View File

@ -1 +0,0 @@
"""Reolink Camera component for HomeAssistant."""

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,366 @@
"""This component updates the camera API and subscription."""
import logging
import re
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import get_url
from homeassistant.helpers.entity_registry import (
async_entries_for_config_entry,
async_get_registry as async_get_entity_registry,
)
from reolink.camera_api import Api
from reolink.subscription_manager import Manager
from .const import (
BASE,
CONF_PLAYBACK_MONTHS,
CONF_PLAYBACK_THUMBNAILS,
CONF_THUMBNAIL_OFFSET,
DEFAULT_PLAYBACK_MONTHS,
DEFAULT_PLAYBACK_THUMBNAILS,
DEFAULT_THUMBNAIL_OFFSET,
EVENT_DATA_RECEIVED,
CONF_CHANNEL,
CONF_MOTION_OFF_DELAY,
CONF_PROTOCOL,
CONF_STREAM,
DEFAULT_CHANNEL,
DEFAULT_MOTION_OFF_DELAY,
DEFAULT_PROTOCOL,
DEFAULT_STREAM,
DEFAULT_TIMEOUT,
DOMAIN,
PUSH_MANAGER,
SESSION_RENEW_THRESHOLD,
)
_LOGGER = logging.getLogger(__name__)
class ReolinkBase:
"""The implementation of the Reolink IP base class."""
def __init__(
self, hass: HomeAssistant, config: dict, options: dict
): # pylint: disable=too-many-arguments
"""Initialize a Reolink camera."""
self._username = config[CONF_USERNAME]
self._password = config[CONF_PASSWORD]
if CONF_CHANNEL not in config:
self._channel = DEFAULT_CHANNEL
else:
self._channel = config[CONF_CHANNEL]
if CONF_TIMEOUT not in options:
self._timeout = DEFAULT_TIMEOUT
else:
self._timeout = options[CONF_TIMEOUT]
if CONF_STREAM not in options:
self._stream = DEFAULT_STREAM
else:
self._stream = options[CONF_STREAM]
if CONF_PROTOCOL not in options:
self._protocol = DEFAULT_PROTOCOL
else:
self._protocol = options[CONF_PROTOCOL]
self._api = Api(
config[CONF_HOST],
config[CONF_PORT],
self._username,
self._password,
channel=self._channel - 1,
stream=self._stream,
protocol=self._protocol,
timeout=self._timeout,
)
self._hass = hass
self.sync_functions = list()
self.motion_detection_state = True
if CONF_MOTION_OFF_DELAY not in options:
self.motion_off_delay = DEFAULT_MOTION_OFF_DELAY
else:
self.motion_off_delay = options[CONF_MOTION_OFF_DELAY]
if CONF_PLAYBACK_MONTHS not in options:
self.playback_months = DEFAULT_PLAYBACK_MONTHS
else:
self.playback_months = options[CONF_PLAYBACK_MONTHS]
if CONF_PLAYBACK_THUMBNAILS not in options:
self.playback_thumbnails = DEFAULT_PLAYBACK_THUMBNAILS
else:
self.playback_thumbnails = options[CONF_PLAYBACK_THUMBNAILS]
if CONF_THUMBNAIL_OFFSET not in options:
self.playback_thumbnail_offset = DEFAULT_THUMBNAIL_OFFSET
else:
self.playback_thumbnail_offset = options[CONF_THUMBNAIL_OFFSET]
@property
def name(self):
"""Create the device name."""
return self._api.name
@property
def unique_id(self):
"""Create the unique ID, base for all entities."""
id = self._api.mac_address.replace(":", "")
return f"{id}-{self.channel}"
@property
def event_id(self):
"""Create the event ID string."""
event_id = self._api.mac_address.replace(":", "")
return f"{EVENT_DATA_RECEIVED}-{event_id}"
@property
def push_manager(self):
"""Create the event ID string."""
push_id = self._api.mac_address.replace(":", "")
return f"{PUSH_MANAGER}-{push_id}"
@property
def timeout(self):
"""Return the timeout setting."""
return self._timeout
@property
def channel(self):
"""Return the channel setting."""
return self._channel
@property
def api(self):
"""Return the API object."""
return self._api
async def connect_api(self):
"""Connect to the Reolink API and fetch initial dataset."""
if not await self._api.get_settings():
return False
if not await self._api.get_states():
return False
await self._api.is_admin()
return True
async def set_channel(self, channel):
"""Set the API channel."""
self._channel = channel
await self._api.set_channel(channel - 1)
async def set_protocol(self, protocol):
"""Set the protocol."""
self._protocol = protocol
await self._api.set_protocol(protocol)
async def set_stream(self, stream):
"""Set the stream."""
self._stream = stream
await self._api.set_stream(stream)
async def set_timeout(self, timeout):
"""Set the API timeout."""
self._timeout = timeout
await self._api.set_timeout(timeout)
async def update_states(self):
"""Call the API of the camera device to update the states."""
await self._api.get_states()
async def update_settings(self):
"""Call the API of the camera device to update the settings."""
await self._api.get_settings()
async def disconnect_api(self):
"""Disconnect from the API, so the connection will be released."""
await self._api.logout()
async def stop(self):
"""Disconnect the API and deregister the event listener."""
await self.disconnect_api()
for func in self.sync_functions:
await self._hass.async_add_executor_job(func)
class ReolinkPush:
"""The implementation of the Reolink IP base class."""
def __init__(
self, hass: HomeAssistant, host, port, username, password
): # pylint: disable=too-many-arguments
"""Initialize a Reolink camera."""
self._host = host
self._port = port
self._username = username
self._password = password
self._hass = hass
self._sman = None
self._webhook_url = None
self._webhook_id = None
self._event_id = None
@property
def sman(self):
"""Return the session manager object."""
return self._sman
async def subscribe(self, event_id):
"""Subscribe to motion events and set the webhook as callback."""
self._event_id = event_id
self._webhook_id = await self.register_webhook()
self._webhook_url = "{}{}".format(
get_url(self._hass, prefer_external=False),
self._hass.components.webhook.async_generate_path(self._webhook_id),
)
self._sman = Manager(self._host, self._port, self._username, self._password)
if await self._sman.subscribe(self._webhook_url):
_LOGGER.info(
"Host %s subscribed successfully to webhook %s",
self._host,
self._webhook_url,
)
await self.set_available(True)
else:
await self.set_available(False)
return True
async def register_webhook(self):
"""
Register a webhook for motion events if it does not exist yet (in case of NVR).
The webhook name (in info) contains the event id (contains mac address op the camera).
So when motion triggers the webhook, it triggers this event. The event is handled by
the binary sensor, in case of NVR the binary sensor also figures out what channel has
the motion. So the flow is: camera onvif event->webhook->HA event->binary sensor.
"""
_LOGGER.debug("Registering webhook for event ID %s", self._event_id)
webhook_id = self._hass.components.webhook.async_generate_id()
self._hass.components.webhook.async_register(
DOMAIN, self._event_id, webhook_id, handle_webhook
)
return webhook_id
async def renew(self):
"""Renew the subscription of the motion events (lease time is set to 15 minutes)."""
if self._sman.renewtimer <= SESSION_RENEW_THRESHOLD:
if not await self._sman.renew():
_LOGGER.error(
"Host %s error renewing the Reolink subscription",
self._host,
)
await self.set_available(False)
await self._sman.subscribe(self._webhook_url)
else:
await self.set_available(True)
else:
await self.set_available(True)
async def set_available(self, available: bool):
"""Set the availability state to the base object."""
self._hass.bus.async_fire(self._event_id, {"available": available})
async def unsubscribe(self):
"""Unsubscribe from the motion events."""
await self.set_available(False)
await self.unregister_webhook()
return await self._sman.unsubscribe()
async def unregister_webhook(self):
"""Unregister the webhook for motion events."""
_LOGGER.debug("Unregistering webhook %s", self._webhook_id)
self._hass.components.webhook.async_unregister(self._webhook_id)
async def count_members(self):
"""Count the number of camera's using this push manager."""
members = 0
for entry_id in self._hass.data[DOMAIN]:
_LOGGER.debug("Got data entry: %s", entry_id)
if PUSH_MANAGER in entry_id:
continue # Count config entries only
try:
base = self._hass.data[DOMAIN][entry_id][BASE]
if base.event_id == self._event_id:
members += 1
except AttributeError:
pass
except KeyError:
pass
_LOGGER.debug("Found %d listeners for event %s", members, self._event_id)
return members
async def handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook from Reolink for inbound messages and calls."""
_LOGGER.debug("Reolink webhook triggered")
if not request.body_exists:
_LOGGER.debug("Webhook triggered without payload")
data = await request.text()
if not data:
_LOGGER.debug("Webhook triggered with unknown payload")
return
_LOGGER.debug(data)
matches = re.findall(r'Name="IsMotion" Value="(.+?)"', data)
if matches:
is_motion = matches[0] == "true"
else:
_LOGGER.debug("Webhook triggered with unknown payload")
return
event_id = await get_event_by_webhook(hass, webhook_id)
if not event_id:
_LOGGER.error("Webhook triggered without event to fire")
hass.bus.async_fire(event_id, {"motion": is_motion})
async def get_webhook_by_event(hass: HomeAssistant, event_id):
"""Find the webhook_id by the event_id."""
try:
handlers = hass.data["webhook"]
except KeyError:
return
for wid, info in handlers.items():
_LOGGER.debug("Webhook: %s", wid)
_LOGGER.debug(info)
if info["name"] == event_id:
return wid
async def get_event_by_webhook(hass: HomeAssistant, webhook_id):
"""Find the event_id by the webhook_id."""
try:
handlers = hass.data["webhook"]
except KeyError:
return
for wid, info in handlers.items():
if wid == webhook_id:
event_id = info["name"]
return event_id

View File

@ -0,0 +1,106 @@
"""This component provides support for Reolink motion events."""
import asyncio
import datetime
import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
from .const import EVENT_DATA_RECEIVED
from .entity import ReolinkEntity
_LOGGER = logging.getLogger(__name__)
DEFAULT_DEVICE_CLASS = "motion"
@asyncio.coroutine
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the Reolink IP Camera switches."""
sensor = MotionSensor(hass, config_entry)
async_add_devices([sensor], update_before_add=False)
class MotionSensor(ReolinkEntity, BinarySensorEntity):
"""An implementation of a Reolink IP camera motion sensor."""
def __init__(self, hass, config):
"""Initialize a the switch."""
ReolinkEntity.__init__(self, hass, config)
BinarySensorEntity.__init__(self)
self._available = False
self._event_state = False
self._last_motion = datetime.datetime.min
@property
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_motion_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return f"{self._base.name} motion"
@property
def is_on(self):
"""Return the state of the sensor."""
if not self._base.motion_detection_state:
self._state = False
return self._state
if self._event_state or self._base.motion_off_delay == 0:
self._state = self._event_state
return self._state
if (
datetime.datetime.now() - self._last_motion
).total_seconds() < self._base.motion_off_delay:
self._state = True
else:
self._state = False
return self._state
@property
def available(self):
"""Return True if entity is available."""
return self._available
@property
def device_class(self):
"""Return the class of this device."""
return DEFAULT_DEVICE_CLASS
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
self.hass.bus.async_listen(self._base.event_id, self.handle_event)
async def handle_event(self, event):
"""Handle incoming event for motion detection and availability."""
try:
self._available = event.data["available"]
return
except KeyError:
pass
if not self._available:
return
try:
self._event_state = event.data["motion"]
except KeyError:
return
if self._base.api.channels > 1:
# Pull the motion state for the NVR channel, it has only 1 event
self._event_state = await self._base.api.get_motion_state()
if self._event_state:
self._last_motion = datetime.datetime.now()
else:
if self._base.motion_off_delay > 0:
await asyncio.sleep(self._base.motion_off_delay)
self.async_schedule_update_ha_state()

View File

@ -1,174 +1,147 @@
"""This component provides basic support for Reolink IP cameras."""
import logging
"""This component provides support for Reolink IP cameras."""
import asyncio
import voluptuous as vol
import datetime
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM, ENTITY_IMAGE_URL
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import config_validation as cv
from datetime import datetime
import logging
from haffmpeg.camera import CameraMjpeg
import voluptuous as vol
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from custom_components.reolink_dev.ReolinkPyPi.camera import ReolinkApi
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_web,
async_get_clientsession,
)
from .const import (
SERVICE_PTZ_CONTROL,
SERVICE_SET_BACKLIGHT,
SERVICE_SET_DAYNIGHT,
SERVICE_SET_SENSITIVITY,
)
from .entity import ReolinkEntity
_LOGGER = logging.getLogger(__name__)
STATE_MOTION = "motion"
STATE_NO_MOTION = "no_motion"
STATE_IDLE = "idle"
DEFAULT_NAME = "Reolink Camera"
DEFAULT_STREAM = "main"
DEFAULT_PROTOCOL = "rtmp"
DEFAULT_CHANNEL = 0
CONF_STREAM = "stream"
CONF_PROTOCOL = "protocol"
CONF_CHANNEL = "channel"
DOMAIN = "camera"
SERVICE_ENABLE_FTP = 'enable_ftp'
SERVICE_DISABLE_FTP = 'disable_ftp'
SERVICE_ENABLE_EMAIL = 'enable_email'
SERVICE_DISABLE_EMAIL = 'disable_email'
SERVICE_ENABLE_IR_LIGHTS = 'enable_ir_lights'
SERVICE_DISABLE_IR_LIGHTS = 'disable_ir_lights'
DEFAULT_BRAND = 'Reolink'
DOMAIN_DATA = 'reolink_devices'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_STREAM, default=DEFAULT_STREAM): vol.In(["main", "sub"]),
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.In(["rtmp", "rtsp"]),
vol.Optional(CONF_CHANNEL, default=DEFAULT_CHANNEL): cv.positive_int,
}
)
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up a Reolink IP Camera."""
host = config.get(CONF_HOST)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
stream = config.get(CONF_STREAM)
protocol = config.get(CONF_PROTOCOL)
channel = config.get(CONF_CHANNEL)
name = config.get(CONF_NAME)
platform = entity_platform.current_platform.get()
camera = ReolinkCamera(hass, config_entry)
session = ReolinkApi(host, channel)
session.login(username, password)
platform.async_register_entity_service(
SERVICE_SET_SENSITIVITY,
{
vol.Required("sensitivity"): cv.positive_int,
vol.Optional("preset"): cv.positive_int,
},
SERVICE_SET_SENSITIVITY,
)
async_add_devices([ReolinkCamera(hass, session, host, username, password, stream, protocol, channel, name)], update_before_add=True)
platform.async_register_entity_service(
SERVICE_SET_DAYNIGHT,
{
vol.Required("mode"): cv.string,
},
SERVICE_SET_DAYNIGHT,
)
# Event enable FTP
def handler_enable_ftp(call):
component = hass.data.get(DOMAIN)
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
platform.async_register_entity_service(
SERVICE_SET_BACKLIGHT,
{
vol.Required("mode"): cv.string,
},
SERVICE_SET_BACKLIGHT,
)
if entity:
entity.enable_ftp_upload()
hass.services.async_register(DOMAIN, SERVICE_ENABLE_FTP, handler_enable_ftp)
platform.async_register_entity_service(
SERVICE_PTZ_CONTROL,
{
vol.Required("command"): cv.string,
vol.Optional("preset"): cv.positive_int,
vol.Optional("speed"): cv.positive_int,
},
SERVICE_PTZ_CONTROL,
)
# Event disable FTP
def handler_disable_ftp(call):
component = hass.data.get(DOMAIN)
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
if entity:
entity.disable_ftp_upload()
hass.services.async_register(DOMAIN, SERVICE_DISABLE_FTP, handler_disable_ftp)
# Event enable email
def handler_enable_email(call):
component = hass.data.get(DOMAIN)
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
if entity:
entity.enable_email()
hass.services.async_register(DOMAIN, SERVICE_ENABLE_EMAIL, handler_enable_email)
# Event disable email
def handler_disable_email(call):
component = hass.data.get(DOMAIN)
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
if entity:
entity.disable_email()
hass.services.async_register(DOMAIN, SERVICE_DISABLE_EMAIL, handler_disable_email)
# Event enable ir lights
def handler_enable_ir_lights(call):
component = hass.data.get(DOMAIN)
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
if entity:
entity.enable_ir_lights()
hass.services.async_register(DOMAIN, SERVICE_ENABLE_IR_LIGHTS, handler_enable_ir_lights)
# Event disable ir lights
def handler_disable_ir_lights(call):
component = hass.data.get(DOMAIN)
entity = component.get_entity(call.data.get(ATTR_ENTITY_ID))
if entity:
entity.disable_ir_lights()
hass.services.async_register(DOMAIN, SERVICE_DISABLE_IR_LIGHTS, handler_disable_ir_lights)
async_add_devices([camera])
class ReolinkCamera(Camera):
class ReolinkCamera(ReolinkEntity, Camera):
"""An implementation of a Reolink IP camera."""
def __init__(self, hass, session, host, username, password, stream, protocol, channel, name):
def __init__(self, hass, config):
"""Initialize a Reolink camera."""
ReolinkEntity.__init__(self, hass, config)
Camera.__init__(self)
super().__init__()
self._host = host
self._username = username
self._password = password
self._stream = stream
self._protocol = protocol
self._channel = channel
self._name = name
self._reolinkSession = session
self._hass = hass
self._manager = self._hass.data[DATA_FFMPEG]
self._last_update = 0
self._ffmpeg = self._hass.data[DATA_FFMPEG]
self._last_image = None
self._last_motion = 0
self._ftp_state = None
self._email_state = None
self._ir_state = None
self._ptzpresets = dict()
self._state = STATE_IDLE
self._ptz_commands = {
"AUTO": "Auto",
"DOWN": "Down",
"FOCUSDEC": "FocusDec",
"FOCUSINC": "FocusInc",
"LEFT": "Left",
"LEFTDOWN": "LeftDown",
"LEFTUP": "LeftUp",
"RIGHT": "Right",
"RIGHTDOWN": "RightDown",
"RIGHTUP": "RightUp",
"STOP": "Stop",
"TOPOS": "ToPos",
"UP": "Up",
"ZOOMDEC": "ZoomDec",
"ZOOMINC": "ZoomInc",
}
self._daynight_modes = {
"AUTO": "Auto",
"COLOR": "Color",
"BLACKANDWHITE": "Black&White",
}
self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.disconnect)
self._backlight_modes = {
"BACKLIGHTCONTROL": "BackLightControl",
"DYNAMICRANGECONTROL": "DynamicRangeControl",
"OFF": "Off",
}
@property
def state_attributes(self):
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_camera_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return self._base.name
@property
def ptz_support(self):
"""Return whether the camera has PTZ support."""
return self._base.api.ptz_support
@property
def device_state_attributes(self):
"""Return the camera state attributes."""
attrs = {"access_token": self.access_tokens[-1]}
attrs = {}
if self._base.api.ptz_support:
attrs["ptz_presets"] = self._base.api.ptz_presets
if self._last_motion:
attrs["last_motion"] = self._last_motion
if self._last_update:
attrs["last_update"] = self._last_update
for key, value in self._backlight_modes.items():
if value == self._base.api.backlight_state:
attrs["backlight_state"] = key
attrs["ftp_enabled"] = self._ftp_state
attrs["email_enabled"] = self._email_state
attrs["ir_lights_enabled"] = self._ir_state
attrs["ptzpresets"] = self._ptzpresets
for key, value in self._daynight_modes.items():
if value == self._base.api.daynight_state:
attrs["daynight_state"] = key
if self._base.api.sensitivity_presets:
attrs["sensitivity"] = self.get_sensitivity_presets()
return attrs
@ -177,136 +150,72 @@ class ReolinkCamera(Camera):
"""Return supported features."""
return SUPPORT_STREAM
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def should_poll(self):
"""Polling needed for the device status."""
return True
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def ftp_state(self):
"""Camera Motion recording Status."""
return self._ftp_state
@property
def email_state(self):
"""Camera email Status."""
return self._email_state
@property
def ptzpresets(self):
"""Camera PTZ presets list."""
return self._ptzpresets
async def stream_source(self):
"""Return the source of the stream."""
if self._protocol == "rtsp":
rtspChannel = f"{self._channel+1:02d}"
stream_source = f"rtsp://{self._username}:{self._password}@{self._host}:{self._reolinkSession.rtspport}/h264Preview_{rtspChannel}_{self._stream}"
else:
stream_source = f"rtmp://{self._host}:{self._reolinkSession.rtmpport}/bcs/channel{self._channel}_{self._stream}.bcs?channel={self._channel}&stream=0&user={self._username}&password={self._password}"
return stream_source
return await self._base.api.get_stream_source()
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
stream_source = await self.stream_source()
stream = CameraMjpeg(self._manager.binary, loop=self._hass.loop)
await stream.open_camera(stream_source)
websession = async_get_clientsession(self._hass)
stream_coro = websession.get(stream_source, timeout=10)
try:
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
self._hass,
request,
stream_reader,
self._manager.ffmpeg_stream_content_type,
)
finally:
await stream.close()
def camera_image(self):
"""Return bytes of camera image."""
return self._reolinkSession.still_image
return await async_aiohttp_proxy_web(self._hass, request, stream_coro)
async def async_camera_image(self):
"""Return a still image response from the camera."""
return self._reolinkSession.snapshot
return await self._base.api.get_snapshot()
def enable_ftp_upload(self):
"""Enable motion ftp recording in camera."""
if self._reolinkSession.set_ftp(True):
self._ftp_state = True
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
async def ptz_control(self, command, **kwargs):
"""Pass PTZ command to the camera."""
if not self.ptz_support:
_LOGGER.error("PTZ is not supported on this device")
return
def disable_ftp_upload(self):
"""Disable motion ftp recording."""
if self._reolinkSession.set_ftp(False):
self._ftp_state = False
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
await self._base.api.set_ptz_command(
command=self._ptz_commands[command], **kwargs
)
def enable_email(self):
"""Enable email motion detection in camera."""
if self._reolinkSession.set_email(True):
self._email_state = True
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
def get_sensitivity_presets(self):
"""Get formatted sensitivity presets."""
presets = list()
preset = dict()
def disable_email(self):
"""Disable email motion detection."""
if self._reolinkSession.set_email(False):
self._email_state = False
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
for api_preset in self._base.api.sensitivity_presets:
preset["id"] = api_preset["id"]
preset["sensitivity"] = api_preset["sensitivity"]
def enable_ir_lights(self):
"""Enable IR lights."""
if self._reolinkSession.set_ir_lights(True):
self._ir_state = True
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
time_string = f'{api_preset["beginHour"]}:{api_preset["beginMin"]}'
begin = datetime.strptime(time_string, "%H:%M")
preset["begin"] = begin.strftime("%H:%M")
def disable_ir_lights(self):
"""Disable IR lights."""
if self._reolinkSession.set_ir_lights(False):
self._ir_state = False
self._hass.states.set(self.entity_id, self.state, self.state_attributes)
time_string = f'{api_preset["endHour"]}:{api_preset["endMin"]}'
end = datetime.strptime(time_string, "%H:%M")
preset["end"] = end.strftime("%H:%M")
async def update_motion_state(self):
if self._reolinkSession.motion_state == True:
self._state = STATE_MOTION
self._last_motion = self._reolinkSession.last_motion
else:
self._state = STATE_NO_MOTION
async def update_status(self):
self._reolinkSession.status()
presets.append(preset.copy())
self._last_update = datetime.datetime.now()
self._ftp_state = self._reolinkSession.ftp_state
self._email_state = self._reolinkSession.email_state
self._ir_state = self._reolinkSession.ir_state
self._ptzpresets = self._reolinkSession.ptzpresets
return presets
def update(self):
"""Update the data from the camera."""
try:
self._hass.loop.create_task(self.update_motion_state())
async def set_sensitivity(self, sensitivity, **kwargs):
"""Set the sensitivity to the camera."""
if "preset" in kwargs:
kwargs["preset"] += 1 # The camera preset ID's on the GUI are always +1
await self._base.api.set_sensitivity(value=sensitivity, **kwargs)
if (self._last_update == 0 or
(datetime.datetime.now() - self._last_update).total_seconds() >= 30):
self._hass.loop.create_task(self.update_status())
async def set_daynight(self, mode):
"""Set the day and night mode to the camera."""
await self._base.api.set_daynight(value=self._daynight_modes[mode])
except Exception as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
async def set_backlight(self, mode):
"""Set the backlight mode to the camera."""
await self._base.api.set_backlight(value=self._backlight_modes[mode])
def disconnect(self, event):
_LOGGER.info("Disconnecting from Reolink camera")
self._reolinkSession.logout()
async def async_enable_motion_detection(self):
"""Predefined camera service implementation."""
self._base.motion_detection_state = True
async def async_disable_motion_detection(self):
"""Predefined camera service implementation."""
self._base.motion_detection_state = False

View File

@ -0,0 +1,217 @@
"""Config flow for the Reolink camera component."""
import logging
import voluptuous as vol
from homeassistant import config_entries, core, data_entry_flow, exceptions
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_TIMEOUT,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from .base import ReolinkBase
from .const import (
BASE,
CONF_CHANNEL,
CONF_MOTION_OFF_DELAY,
CONF_PLAYBACK_MONTHS,
CONF_PLAYBACK_THUMBNAILS,
CONF_PROTOCOL,
CONF_STREAM,
CONF_THUMBNAIL_OFFSET,
DEFAULT_MOTION_OFF_DELAY,
DEFAULT_PLAYBACK_MONTHS,
DEFAULT_PLAYBACK_THUMBNAILS,
DEFAULT_PROTOCOL,
DEFAULT_STREAM,
DEFAULT_THUMBNAIL_OFFSET,
DEFAULT_TIMEOUT,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Reolink camera's."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
channels = 1
mac_address = None
base = None
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return ReolinkOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
self.data = user_input
try:
self.info = await self.async_validate_input(self.hass, user_input)
if self.channels > 1:
return await self.async_step_nvr()
self.data[CONF_CHANNEL] = 1
await self.async_set_unique_id(
f"{self.mac_address}{user_input[CONF_CHANNEL]}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=self.info["title"], data=self.data)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidHost:
errors["host"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PORT, default=80): cv.positive_int,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
)
async def async_step_nvr(self, user_input=None):
"""Configure a NVR with multiple channels."""
errors = {}
if user_input is not None:
self.data.update(user_input)
await self.async_set_unique_id(
f"{self.mac_address}{user_input[CONF_CHANNEL]}"
)
self._abort_if_unique_id_configured()
await self.base.set_channel(user_input[CONF_CHANNEL])
await self.base.update_settings()
return self.async_create_entry(title=self.base.name, data=self.data)
return self.async_show_form(
step_id="nvr",
data_schema=vol.Schema(
{
vol.Required(CONF_CHANNEL): vol.All(
vol.Coerce(int), vol.Range(min=1, max=self.channels)
),
}
),
errors=errors,
)
async def async_validate_input(self, hass: core.HomeAssistant, user_input: dict):
"""Validate the user input allows us to connect."""
self.base = ReolinkBase(hass, user_input, [])
if not await self.base.connect_api():
raise CannotConnect
title = self.base.api.name
self.channels = self.base.api.channels
self.mac_address = self.base.api.mac_address
return {"title": title}
async def async_finish_flow(self, flow, result):
"""Finish flow."""
# if result['type'] == data_entry_flow.RESULT_TYPE_ABORT:
self.base.disconnect_api()
class ReolinkOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Reolink options."""
def __init__(self, config_entry):
"""Initialize Reolink options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None): # pylint: disable=unused-argument
"""Manage the Reolink options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_PROTOCOL,
default=self.config_entry.options.get(
CONF_PROTOCOL, DEFAULT_PROTOCOL
),
): vol.In(["rtmp", "rtsp"]),
vol.Required(
CONF_STREAM,
default=self.config_entry.options.get(
CONF_STREAM, DEFAULT_STREAM
),
): vol.In(["main", "sub"]),
vol.Required(
CONF_MOTION_OFF_DELAY,
default=self.config_entry.options.get(
CONF_MOTION_OFF_DELAY, DEFAULT_MOTION_OFF_DELAY
),
): vol.All(vol.Coerce(int), vol.Range(min=0)),
vol.Required(
CONF_PLAYBACK_MONTHS,
default=self.config_entry.options.get(
CONF_PLAYBACK_MONTHS, DEFAULT_PLAYBACK_MONTHS
),
): cv.positive_int,
vol.Optional(
CONF_PLAYBACK_THUMBNAILS,
default=self.config_entry.options.get(
CONF_PLAYBACK_THUMBNAILS, DEFAULT_PLAYBACK_THUMBNAILS
),
): cv.boolean,
vol.Optional(
CONF_THUMBNAIL_OFFSET,
default=self.config_entry.options.get(
CONF_THUMBNAIL_OFFSET, DEFAULT_THUMBNAIL_OFFSET
),
): vol.All(vol.Coerce(int), vol.Range(min=0, max=60)),
vol.Optional(
CONF_TIMEOUT,
default=self.config_entry.options.get(
CONF_TIMEOUT, DEFAULT_TIMEOUT
),
): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)),
}
),
)
class AlreadyConfigured(exceptions.HomeAssistantError):
"""Error to indicate device is already configured."""
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidHost(exceptions.HomeAssistantError):
"""Error to indicate there is an invalid hostname."""

View File

@ -0,0 +1,31 @@
"""Constants for the Reolink Camera integration."""
DOMAIN = "reolink_dev"
DOMAIN_DATA = "reolink_dev_devices"
EVENT_DATA_RECEIVED = "reolink_dev-event"
COORDINATOR = "coordinator"
BASE = "base"
PUSH_MANAGER = "push_manager"
SESSION_RENEW_THRESHOLD = 300
CONF_STREAM = "stream"
CONF_PROTOCOL = "protocol"
CONF_CHANNEL = "channel"
CONF_MOTION_OFF_DELAY = "motion_off_delay"
CONF_PLAYBACK_MONTHS = "playback_months"
CONF_PLAYBACK_THUMBNAILS = "playback_thumbnails"
CONF_THUMBNAIL_OFFSET = "playback_thumbnail_offset"
DEFAULT_CHANNEL = 1
DEFAULT_MOTION_OFF_DELAY = 60
DEFAULT_PROTOCOL = "rtmp"
DEFAULT_STREAM = "main"
DEFAULT_TIMEOUT = 30
DEFAULT_PLAYBACK_MONTHS = 2
DEFAULT_PLAYBACK_THUMBNAILS = False
DEFAULT_THUMBNAIL_OFFSET = 6
SERVICE_PTZ_CONTROL = "ptz_control"
SERVICE_SET_BACKLIGHT = "set_backlight"
SERVICE_SET_DAYNIGHT = "set_daynight"
SERVICE_SET_SENSITIVITY = "set_sensitivity"

View File

@ -0,0 +1,41 @@
"""Reolink parent entity class."""
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import BASE, COORDINATOR, DOMAIN
class ReolinkEntity(CoordinatorEntity):
"""Parent class for Reolink Entities."""
def __init__(self, hass, config):
"""Initialize common aspects of a Reolink entity."""
coordinator = hass.data[DOMAIN][config.entry_id][COORDINATOR]
super().__init__(coordinator)
self._base = hass.data[DOMAIN][config.entry_id][BASE]
self._hass = hass
self._state = False
@property
def device_info(self):
"""Information about this entity/device."""
return {
"identifiers": {(DOMAIN, self._base.unique_id)},
"connections": {(CONNECTION_NETWORK_MAC, self._base.api.mac_address)},
"name": self._base.name,
"sw_version": self._base.api.sw_version,
"model": self._base.api.model,
"manufacturer": self._base.api.manufacturer,
"channel": self._base.channel
}
@property
def available(self):
"""Return True if entity is available."""
return self._base.api.session_active
async def request_refresh(self):
"""Call the coordinator to update the API."""
await self.coordinator.async_request_refresh()

View File

@ -1,8 +1,25 @@
{
"domain": "reolink_dev",
"name": "Reolink IP camera",
"documentation": "https://www.example.com",
"dependencies": ["ffmpeg"],
"codeowners": ["@fwestenberg"],
"requirements": ["aiosmtpd==1.2"]
}
"domain": "reolink_dev",
"name": "Reolink IP camera",
"documentation": "https://github.com/fwestenberg/reolink_dev",
"issue_tracker": "https://github.com/fwestenberg/reolink_dev/issues",
"version": "0.15",
"requirements": [
"reolink==0.0.17"
],
"dependencies": [
"ffmpeg",
"webhook"
],
"after_dependencies": [
"media_source",
"http"
],
"codeowners": [
"@fwestenberg"
],
"config_flow": true,
"ssdp": [],
"zeroconf": [],
"homekit": {}
}

View File

@ -0,0 +1,412 @@
"""Reolink Camera Media Source Implementation."""
from urllib import parse
import secrets
import datetime as dt
import logging
from typing import Optional, Tuple
from aiohttp import web
from haffmpeg.tools import IMAGE_JPEG
from dateutil import relativedelta
from homeassistant.core import HomeAssistant, callback
import homeassistant.util.dt as dt_utils
from homeassistant.components.http import HomeAssistantView
# from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_VIDEO,
)
from homeassistant.components.media_source.const import MEDIA_MIME_TYPES
from homeassistant.components.media_source.error import MediaSourceError, Unresolvable
from homeassistant.components.media_source.models import (
BrowseMediaSource,
MediaSource,
MediaSourceItem,
PlayMedia,
)
from homeassistant.components.stream import create_stream
from homeassistant.components.ffmpeg import async_get_image
from custom_components.reolink_dev.base import ReolinkBase
from . import typings
from .const import BASE, DEFAULT_THUMBNAIL_OFFSET, DOMAIN
_LOGGER = logging.getLogger(__name__)
# MIME_TYPE = "rtmp/mp4"
# MIME_TYPE = "video/mp4"
MIME_TYPE = "application/x-mpegURL"
NAME = "Reolink IP Camera"
class IncompatibleMediaSource(MediaSourceError):
"""Incompatible media source attributes."""
async def async_get_media_source(hass: HomeAssistant):
"""Set up Reolink media source."""
_LOGGER.debug("Creating REOLink Media Source")
source = ReolinkSource(hass)
hass.http.register_view(ReolinkSourceThumbnailView(hass, source))
return source
class ReolinkSource(MediaSource):
"""Provide Reolink camera recordings as media sources."""
name: str = NAME
def __init__(self, hass: HomeAssistant):
"""Initialize Reolink source."""
super().__init__(DOMAIN)
self.hass = hass
self.cache = {}
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve a media item to a playable item."""
_, camera_id, event_id = async_parse_identifier(item)
cache: typings.MediaSourceCacheEntry = self.cache[camera_id]
event = cache["playback_events"][event_id]
base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE]
url = await base.api.get_vod_source(event["file"])
_LOGGER.debug("Load VOD %s", url)
stream = create_stream(self.hass, url)
stream.add_provider("hls", timeout=600)
url: str = stream.endpoint_url("hls")
# the media browser seems to have a problem with the master_playlist
# ( it does not load the referenced playlist ) so we will just
# force the reference playlist instead, this seems to work
# though technically wrong
url = url.replace("master_", "")
_LOGGER.debug("Proxy %s", url)
return PlayMedia(url, MIME_TYPE)
async def async_browse_media(
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMediaSource:
"""Browse media."""
try:
source, camera_id, event_id = async_parse_identifier(item)
except Unresolvable as err:
raise BrowseError(str(err)) from err
_LOGGER.debug("Browsing %s, %s, %s", source, camera_id, event_id)
if camera_id and camera_id not in self.cache:
raise BrowseError("Camera does not exist.")
if (
event_id
and not "/" in event_id
and event_id not in self.cache[camera_id]["playback_events"]
):
raise BrowseError("Event does not exist.")
return await self._async_browse_media(source, camera_id, event_id, False)
async def _async_browse_media(
self, source: str, camera_id: str, event_id: str = None, no_descend: bool = True
) -> BrowseMediaSource:
""" actual browse after input validation """
event: typings.VodEvent = None
cache: typings.MediaSourceCacheEntry = None
start_date = None
if camera_id and camera_id in self.cache:
cache = self.cache[camera_id]
if cache and event_id:
if "playback_events" in cache and event_id in cache["playback_events"]:
event = cache["playback_events"][event_id]
end_date = event["end"]
start_date = event["start"]
time = start_date.time()
duration = end_date - start_date
title = f"{time} {duration}"
else:
year, *rest = event_id.split("/", 3)
month = rest[0] if len(rest) > 0 else None
day = rest[1] if len(rest) > 1 else None
start_date = dt.datetime.combine(
dt.date(
int(year), int(month) if month else 1, int(day) if day else 1
),
dt.time.min,
dt_utils.now().tzinfo,
)
title = f"{start_date.date()}"
path = f"{source}/{camera_id}/{event_id}"
else:
if cache is None:
camera_id = ""
title = NAME
else:
title = cache["name"]
path = f"{source}/{camera_id}"
media_class = MEDIA_CLASS_DIRECTORY if event is None else MEDIA_CLASS_VIDEO
media = BrowseMediaSource(
domain=DOMAIN,
identifier=path,
media_class=media_class,
media_content_type=MEDIA_TYPE_VIDEO,
title=title,
can_play=bool(not event is None and event.get("file")),
can_expand=event is None,
)
if not event is None and cache.get("playback_thumbnails", False):
url = "/api/" + DOMAIN + f"/media_proxy/{camera_id}/{event_id}"
# TODO : I cannot find a way to get the current user context at this point
# so I will have to leave the view as unauthenticated, as a temporary
# security measure, I will add a unique token to the event to limit
# "exposure"
# url = async_sign_path(self.hass, None, url, dt.timedelta(minutes=30))
if "token" not in event:
event["token"] = secrets.token_hex()
media.thumbnail = f"{url}?token={parse.quote_plus(event['token'])}"
if not media.can_play and not media.can_expand:
_LOGGER.debug(
"Camera %s with event %s without media url found", camera_id, event_id
)
raise IncompatibleMediaSource
if not media.can_expand or no_descend:
return media
media.children = []
base: ReolinkBase = None
if cache is None:
for entry_id in self.hass.data[DOMAIN]:
entry = self.hass.data[DOMAIN][entry_id]
if not isinstance(entry, dict) or not BASE in entry:
continue
base = entry[BASE]
camera_id = base.unique_id
cache = self.cache.get(camera_id, None)
if cache is None:
cache = self.cache[camera_id] = {
"entry_id": entry_id,
"unique_id": base.unique_id,
"playback_events": {},
}
cache["name"] = base.name
child = await self._async_browse_media(source, camera_id)
media.children.append(child)
return media
base = self.hass.data[DOMAIN][cache["entry_id"]][BASE]
# TODO: the cache is one way so over time it can grow and have invalid
# records, the code should be expanded to invalidate/expire
# entries
if base is None:
raise BrowseError("Camera does not exist.")
if not start_date:
if (
"playback_day_entries" not in cache
or cache.get("playback_months", -1) != base.playback_months
):
end_date = dt_utils.now()
start_date = dt.datetime.combine(end_date.date(), dt.time.min)
cache["playback_months"] = base.playback_months
if cache["playback_months"] > 1:
start_date -= relativedelta.relativedelta(
months=int(cache["playback_months"])
)
entries = cache["playback_day_entries"] = []
search, _ = await base.api.send_search(start_date, end_date, True)
if not search is None:
for status in search:
year = status["year"]
month = status["mon"]
for day, flag in enumerate(status["table"], start=1):
if flag == "1":
entries.append(dt.date(year, month, day))
entries.sort()
else:
entries = cache["playback_day_entries"]
for date in cache["playback_day_entries"]:
child = await self._async_browse_media(
source, camera_id, f"{date.year}/{date.month}/{date.day}"
)
media.children.append(child)
return media
cache["playback_thumbnails"] = base.playback_thumbnails
end_date = dt.datetime.combine(
start_date.date(), dt.time.max, start_date.tzinfo
)
_, files = await base.api.send_search(start_date, end_date)
if not files is None:
events = cache.setdefault("playback_events", {})
for file in files:
dto = file["EndTime"]
end_date = dt.datetime(
dto["year"],
dto["mon"],
dto["day"],
dto["hour"],
dto["min"],
dto["sec"],
0,
end_date.tzinfo,
)
dto = file["StartTime"]
start_date = dt.datetime(
dto["year"],
dto["mon"],
dto["day"],
dto["hour"],
dto["min"],
dto["sec"],
0,
end_date.tzinfo,
)
event_id = str(start_date.timestamp())
event = events.setdefault(event_id, {})
event["start"] = start_date
event["end"] = end_date
event["file"] = file["name"]
child = await self._async_browse_media(source, camera_id, event_id)
media.children.append(child)
return media
class ReolinkSourceThumbnailView(HomeAssistantView):
""" Thumbnial view handler """
url = "/api/" + DOMAIN + "/media_proxy/{camera_id}/{event_id}"
name = "api:" + DOMAIN + ":image"
requires_auth = False
cors_allowed = True
def __init__(self, hass: HomeAssistant, source: ReolinkSource):
"""Initialize media view """
self.hass = hass
self.source = source
async def get(
self, request: web.Request, camera_id: str, event_id: str
) -> web.Response:
""" start a GET request. """
if not camera_id or not event_id:
raise web.HTTPNotFound()
cache: typings.MediaSourceCacheEntry = self.source.cache.get(camera_id, None)
if cache is None or "playback_events" not in cache:
_LOGGER.debug("camera %s not found", camera_id)
raise web.HTTPNotFound()
event = cache["playback_events"].get(event_id, None)
if event is None:
_LOGGER.debug("camera %s, event %s not found", camera_id, event_id)
raise web.HTTPNotFound()
token = request.query.get("token")
if (token and event.get("token") != token) or (
not token and not self.requires_auth
):
_LOGGER.debug(
"invalid or missing token %s for camera %s, event %s",
token,
camera_id,
event_id,
)
raise web.HTTPNotFound()
_LOGGER.debug("thumbnail %s, %s", camera_id, event_id)
base: ReolinkBase = self.hass.data[DOMAIN][cache["entry_id"]][BASE]
image = event.get("thumbnail", None)
if (
image is None
or cache.get("playback_thumbnail_offset", DEFAULT_THUMBNAIL_OFFSET)
!= base.playback_thumbnail_offset
):
cache["playback_thumbnails"] = base.playback_thumbnails
cache["playback_thumbnail_offset"] = base.playback_thumbnail_offset
if not cache["playback_thumbnails"]:
_LOGGER.debug("Thumbnails not allowed on camera %s", camera_id)
raise web.HTTPInternalServerError()
_LOGGER.debug("generating thumbnail for %s, %s", camera_id, event_id)
extra_cmd: str = None
if cache["playback_thumbnail_offset"] > 0:
extra_cmd = f"-ss {cache['playback_thumbnail_offset']}"
image = event["thumbail"] = await async_get_image(
self.hass,
await base.api.get_vod_source(event["file"]),
extra_cmd=extra_cmd,
)
_LOGGER.debug("generated thumbnail for %s, %s", camera_id, event_id)
if image:
return web.Response(body=image, content_type=IMAGE_JPEG)
_LOGGER.debug(
"No thumbnail generated for camera %s, event %s", camera_id, event_id
)
raise web.HTTPInternalServerError()
@callback
def async_parse_identifier(
item: MediaSourceItem,
) -> Tuple[str, str, Optional[str]]:
"""Parse identifier."""
if not item.identifier:
return "events", "", None
source, path = item.identifier.lstrip("/").split("/", 1)
if source != "events":
raise Unresolvable("Unknown source directory.")
if "/" in path:
camera_id, event_id = path.split("/", 1)
return source, camera_id, event_id
return source, path, None

View File

@ -1,41 +1,84 @@
enable_ftp:
description: Enable FTP upload on motion recording.
ptz_control:
name: Pan/Zoom/Tilt Control
description: Execute a PTZ command.
target:
entity:
integration: reolink_dev
domain: camera
fields:
entity_id:
description: Name of the Reolink camera entity to set.
description: Name(s) of the Reolink camera entity to execute the command on.
example: 'camera.frontdoor'
disable_ftp:
description: Disable FTP upload on motion recording.
command:
description: >-
Command to execute. Possible values are:
AUTO DOWN FOCUSDEC FOCUSINC LEFT LEFTDOWN LEFTUP
RIGHT RIGHTDOWN RIGHTUP STOP TOPOS UP ZOOMDEC ZOOMINC
example: LEFTUP
preset:
description: (Optional) In case of the command TOPOS. The available presets are listed as attribute on the camera.
example: HOME
speed:
description: (Optional) Speed at which the movement takes place.
example: 25
set_sensitivity:
name: Set Motion Sensitivity
description: Set the motion detection sensitivity.
target:
entity:
integration: reolink_dev
domain: camera
fields:
entity_id:
description: Name of the Reolink camera entity to set.
description: Name(s) of the Reolink camera entity to execute the command on.
example: 'camera.frontdoor'
enable_email:
description: Enable email functionality on motion detection.
sensitivity:
description: New sensitivity, value between 1 (low sensitivity) and 50 (high sensitivity)
example: 25
preset:
description: >-
(Optional) Set the sensitivity of a specific preset (time schedule). When no value is supplied,
all presets will be changed.
set_daynight:
name: Set Day/Night Mode
description: Set day and night parameter.
target:
entity:
integration: reolink_dev
domain: camera
fields:
entity_id:
description: Name of the Reolink camera entity to set.
description: Name(s) of the Reolink camera entity to execute the command on.
example: 'camera.frontdoor'
disable_email:
description: Disable email functionality on motion detection.
mode:
description: >-
The day and night mode parameter supports the following values:
AUTO: Auto switch between black & white mode
COLOR: Always record videos in color mode
BLACKANDWHITE: Always record videos in black & white mode
example: AUTO
set_backlight:
name: Set backlight
description: >-
Optimizing brightness and contrast levels to compensate for differences
between dark and bright objects using either BLC or WDR mode.
This may improve image clarity in high contrast situations,
but it should be tested at different times of the day and night to ensure there is no negative effect.
target:
entity:
integration: reolink_dev
domain: camera
fields:
entity_id:
description: Name of the Reolink camera entity to set.
description: Name(s) of the Reolink camera entity to execute the command on.
example: 'camera.frontdoor'
enable_ir_lights:
description: Enable the infrared lights (nightvision) of the Reolink camera.
fields:
entity_id:
description: Name of the Reolink camera entity to set.
example: 'camera.frontdoor'
disable_ir_lights:
description: Disable the infrared lights (nightvision) of the Reolink camera.
fields:
entity_id:
description: Name of the Reolink camera entity to set.
example: 'camera.frontdoor'
mode:
description: >-
The backlight parameter supports the following values:
BACKLIGHTCONTROL: use Backlight Control
DYNAMICRANGECONTROL: use Dynamic Range Control
OFF: no optimization
example: DYNAMICRANGECONTROL

View File

@ -0,0 +1,42 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"nvr": {
"data": {
"channel": "Channel"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protocol",
"stream": "Stream",
"timeout": "Timeout",
"motion_off_delay": "Motion sensor off delay (seconds)",
"playback_months": "Playback range (months)",
"playback_thumbnails": "Create thumbnails for playback items",
"playback_thumbnail_offset": "Pre-Record offset (seconds) for thumbnail"
}
}
}
}
}

View File

@ -0,0 +1,269 @@
"""This component provides support many for Reolink IP cameras switches."""
import asyncio
import logging
from homeassistant.components.switch import DEVICE_CLASS_SWITCH
from homeassistant.helpers.entity import ToggleEntity
from .const import BASE, DOMAIN
from .entity import ReolinkEntity
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
async def async_setup_entry(hass, config_entry, async_add_devices):
"""Set up the Reolink IP Camera switches."""
devices = []
base = hass.data[DOMAIN][config_entry.entry_id][BASE]
for capability in await base.api.get_switch_capabilities():
if capability == "ftp":
devices.append(FTPSwitch(hass, config_entry))
elif capability == "email":
devices.append(EmailSwitch(hass, config_entry))
elif capability == "audio":
devices.append(AudioSwitch(hass, config_entry))
elif capability == "irLights":
devices.append(IRLightsSwitch(hass, config_entry))
elif capability == "recording":
devices.append(RecordingSwitch(hass, config_entry))
else:
continue
async_add_devices(devices, update_before_add=False)
class FTPSwitch(ReolinkEntity, ToggleEntity):
"""An implementation of a Reolink IP camera FTP switch."""
def __init__(self, hass, config):
"""Initialize a Reolink camera."""
ReolinkEntity.__init__(self, hass, config)
ToggleEntity.__init__(self)
@property
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_ftpSwitch_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return f"{self._base.name} FTP"
@property
def is_on(self):
"""Camera Motion FTP upload Status."""
return self._base.api.ftp_state
@property
def device_class(self):
"""Device class of the switch."""
return DEVICE_CLASS_SWITCH
@property
def icon(self):
"""Icon of the switch."""
if self.is_on:
return "mdi:folder-upload"
return "mdi:folder-remove"
async def async_turn_on(self, **kwargs):
"""Enable motion ftp recording."""
await self._base.api.set_ftp(True)
await self.request_refresh()
async def async_turn_off(self, **kwargs):
"""Disable motion ftp recording."""
await self._base.api.set_ftp(False)
await self.request_refresh()
class EmailSwitch(ReolinkEntity, ToggleEntity):
"""An implementation of a Reolink IP camera email switch."""
def __init__(self, hass, config):
"""Initialize a Reolink camera."""
ReolinkEntity.__init__(self, hass, config)
ToggleEntity.__init__(self)
@property
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_emailSwitch_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return f"{self._base.name} email"
@property
def is_on(self):
"""Camera Motion email upload Status."""
return self._base.api.email_state
@property
def device_class(self):
"""Device class of the switch."""
return DEVICE_CLASS_SWITCH
@property
def icon(self):
"""Icon of the switch."""
if self.is_on:
return "mdi:email"
return "mdi:email-outline"
async def async_turn_on(self, **kwargs):
"""Enable motion email notification."""
await self._base.api.set_email(True)
await self.request_refresh()
async def async_turn_off(self, **kwargs):
"""Disable motion email notification."""
await self._base.api.set_email(False)
await self.request_refresh()
class IRLightsSwitch(ReolinkEntity, ToggleEntity):
"""An implementation of a Reolink IP camera ir lights switch."""
def __init__(self, hass, config):
"""Initialize a Reolink camera."""
ReolinkEntity.__init__(self, hass, config)
ToggleEntity.__init__(self)
@property
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_irLightsSwitch_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return f"{self._base.name} IR lights"
@property
def is_on(self):
"""Camera Motion ir lights Status."""
return self._base.api.ir_state
@property
def device_class(self):
"""Device class of the switch."""
return DEVICE_CLASS_SWITCH
@property
def icon(self):
"""Icon of the switch."""
if self.is_on:
return "mdi:flashlight"
return "mdi:flashlight-off"
async def async_turn_on(self, **kwargs):
"""Enable motion ir lights."""
await self._base.api.set_ir_lights(True)
await self.request_refresh()
async def async_turn_off(self, **kwargs):
"""Disable motion ir lights."""
await self._base.api.set_ir_lights(False)
await self.request_refresh()
class RecordingSwitch(ReolinkEntity, ToggleEntity):
"""An implementation of a Reolink IP camera recording switch."""
def __init__(self, hass, config):
"""Initialize a Reolink camera."""
ReolinkEntity.__init__(self, hass, config)
ToggleEntity.__init__(self)
@property
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_recordingSwitch_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return f"{self._base.name} recording"
@property
def is_on(self):
"""Camera recording upload Status."""
return self._base.api.recording_state
@property
def device_class(self):
"""Device class of the switch."""
return DEVICE_CLASS_SWITCH
@property
def icon(self):
"""Icon of the switch."""
if self.is_on:
return "mdi:filmstrip"
return "mdi:filmstrip-off"
async def async_turn_on(self, **kwargs):
"""Enable recording."""
await self._base.api.set_recording(True)
await self.request_refresh()
async def async_turn_off(self, **kwargs):
"""Disable recording."""
await self._base.api.set_recording(False)
await self.request_refresh()
class AudioSwitch(ReolinkEntity, ToggleEntity):
"""An implementation of a Reolink IP camera audio switch."""
def __init__(self, hass, config):
"""Initialize a Reolink camera."""
ReolinkEntity.__init__(self, hass, config)
ToggleEntity.__init__(self)
@property
def unique_id(self):
"""Return Unique ID string."""
return f"reolink_audioSwitch_{self._base.unique_id}"
@property
def name(self):
"""Return the name of this camera."""
return f"{self._base.name} record audio"
@property
def is_on(self):
"""Camera audio switch Status."""
return self._base.api.audio_state
@property
def device_class(self):
"""Device class of the switch."""
return DEVICE_CLASS_SWITCH
@property
def icon(self):
"""Icon of the switch."""
if self.is_on:
return "mdi:volume-high"
return "mdi:volume-off"
async def async_turn_on(self, **kwargs):
"""Enable audio recording."""
await self._base.api.set_audio(True)
await self.request_refresh()
async def async_turn_off(self, **kwargs):
"""Disable audio recording."""
await self._base.api.set_audio(False)
await self.request_refresh()

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Diese Kamera ist bereits konfiguriert"
},
"error": {
"cannot_connect": "Verbindung zur Kamera konnte nicht hergestellt werden",
"invalid_auth": "Benutzername oder Passwort fehlerhaft",
"unknown": "Unerwarteter Fehler"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port",
"username": "Benutzername",
"password": "Passwort"
}
},
"nvr": {
"data": {
"channel": "Kanal"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protokoll",
"stream": "Stream",
"timeout": "Timeout (Sekunden)",
"motion_off_delay": "Bewegungssensor Ausschaltverzögerung (Sekunden)"
}
}
}
}
}

View File

@ -0,0 +1,42 @@
{
"config": {
"abort": {
"already_configured": "This camera is already configured"
},
"error": {
"cannot_connect": "Failed to connect with the camera",
"invalid_auth": "Invalid username or password",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port",
"username": "Username",
"password": "Password"
}
},
"nvr": {
"data": {
"channel": "Channel"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protocol",
"stream": "Stream",
"timeout": "Timeout (seconds)",
"motion_off_delay": "Motion sensor off delay (seconds)",
"playback_months": "Playback range (months)",
"playback_thumbnails": "Create thumbnails for playback items",
"playback_thumbnail_offset": "Pre-Record offset (seconds) for thumbnail"
}
}
}
}
}

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Esta cámara ya ha sido configurada"
},
"error": {
"cannot_connect": "No se pudo conectar con la cámara",
"invalid_auth": "Nombre de usuario o contraseña incorrecta",
"unknown": "Error inesperado"
},
"step": {
"user": {
"data": {
"host": "Dirección",
"port": "Puerto",
"username": "Usuario",
"password": "Contraseña"
}
},
"nvr": {
"data": {
"channel": "Canal"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protocolo",
"stream": "Transferencia",
"timeout": "Tiempo fuera (segundos)",
"motion_off_delay": "Sensor de movimiento apagado retardo (segundos)"
}
}
}
}
}

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Cette caméra est déjà configurée"
},
"error": {
"cannot_connect": "Impossible de se connecter à la caméra",
"invalid_auth": "Mauvais nom d'utilisateur et/ou mot de passe",
"unknown": "Erreur inconnue"
},
"step": {
"user": {
"data": {
"host": "Hôte",
"port": "Port",
"username": "Nom d'utilisateur",
"password": "Mot de passe"
}
},
"nvr": {
"data": {
"channel": "Chaîne"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protocole",
"stream": "Flux",
"timeout": "Temporisation (en secondes)",
"motion_off_delay": "Délai de désactivation du capteur de mouvements (en secondes)"
}
}
}
}
}

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "כבר מקונפג"
},
"error": {
"cannot_connect": "אין אפשרות להתחבר",
"invalid_auth": "הרשאה לא נכונה",
"unknown": "בעיה לא ידועה"
},
"step": {
"user": {
"data": {
"host": "שרת",
"port": "פורט",
"username": "שם משתמש",
"password": "סיסמא"
}
},
"nvr": {
"data": {
"channel": "ערוץ"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "פרוטוקול",
"stream": "סטרים",
"timeout": "טיימאאוט",
"motion_off_delay": "השהיית כבוי תזוזה"
}
}
}
}
}

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Deze camera is al geconfigureerd"
},
"error": {
"cannot_connect": "Kan niet verbinden met de camera",
"invalid_auth": "Ongeldige gebruikersnaam of wachtwoord",
"unknown": "Onbekende fout opgetreden"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Poort",
"username": "Gebruikersnaam",
"password": "Wachtwoord"
}
},
"nvr": {
"data": {
"channel": "Kanaal"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protocol",
"stream": "Stream",
"timeout": "Timeout (seconden)",
"motion_off_delay": "Bewegingssensor uit vertraging (seconden)"
}
}
}
}
}

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Ta kamera jest już skonfigurowana"
},
"error": {
"cannot_connect": "Nie udało się połączyć z kamerą",
"invalid_auth": "Nieprawidłowy użytkownik lub hasło",
"unknown": "Niespodziewany błąd"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port",
"username": "Nazwa użytkownika",
"password": "Hasło"
}
},
"nvr": {
"data": {
"channel": "Kanał"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protokół",
"stream": "Stream",
"timeout": "Timeout (sekundy)",
"motion_off_delay": "Opóźnienie wyłączenia czujnika ruchu (sekundy)"
}
}
}
}
}

View File

@ -0,0 +1,39 @@
{
"config": {
"abort": {
"already_configured": "Denna kamera är redan konfiguerad"
},
"error": {
"cannot_connect": "Misslyckades med att ansluta till kameran",
"invalid_auth": "Fel användarnamn eller lösenord",
"unknown": "Oförväntat fel"
},
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port",
"username": "Användarnamn",
"password": "Lösenord"
}
},
"nvr": {
"data": {
"channel": "Kanal"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"protocol": "Protokoll",
"stream": "Ström",
"timeout": "Timeout (sekunder)",
"motion_off_delay": "Rörelsesensor avstängningfördröjning (sekunder)"
}
}
}
}
}

View File

@ -0,0 +1,31 @@
""" Typing declarations for strongly typed dictionaries """
from typing import Any, Dict, List, TypedDict
from datetime import datetime, date
VodEvent = TypedDict(
"VodEvent",
{
"start": datetime,
"end": datetime,
"file": str,
"thumbnail": Any,
},
total=False,
)
MediaSourceCacheEntry = TypedDict(
"MediaSourceCacheEntry",
{
"entry_id": str,
"unique_id": str,
"event_id": str,
"name": str,
"playback_months": int,
"playback_thumbnails": bool,
"playback_thumbnail_offset": int,
"playback_day_entries": List[date],
"playback_events": Dict[str, VodEvent],
},
total=False,
)

View File

@ -1,28 +1,16 @@
# Icons @ https://materialdesignicons.com/
# FontAwesome implementation @ https://github.com/thomasloven/hass-fontawesome
title: Making Home Great Again
views:
- !include config/views/overview.yaml
- !include config/views/floorplan.yaml
- !include config/views/lights.yaml
- !include config/views/livingroom.yaml
- !include config/views/media.yaml
- !include config/views/robots.yaml
- !include config/views/humidity.yaml
- !include config/views/spotify.yaml
- !include config/views/floorplan.yaml
- !include config/views/instagram.yaml
- !include config/views/devices.yaml
- !include config/views/landroid.yaml
- !include config/views/cctv.yaml
- !include config/views/network.yaml
- !include config/views/alerts.yaml
resources:
- url: /local/lovelace/resources/weather-card.js
type: module
- url: /local/lovelace/resources/auto-entities.js
type: module
- url: /local/lovelace/resources/color-lite-card.js
type: js
# https://github.com/bradcrc/color-lite-card/
- url: /local/lovelace/resources/now-playing-card.js
type: js
# https://github.com/bradcrc/Now-Playing-Card

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,69 @@
class ColorLite extends HTMLElement {
set hass(hass) {
if (!this.content) {
const card = document.createElement('ha-card');
this.content = document.createElement('div');
card.appendChild(this.content);
card.style.background = 'none';
this.appendChild(card);
}
const entityId = this.config.entity;
const state = hass.states[entityId];
// if the light is on
if(state){
if(state.state == 'on'){
const imageURLId = this.config.image;
var ImURL = imageURLId;
const imageURLCId = this.config.color_image;
var rgbval = state.attributes.rgb_color;
var hsval = state.attributes.hs_color;
var hsar = "";
var min_bright = (this.config.min_brightness * 2.5);
var bright = state.attributes.brightness;
if (hsval) {
if (rgbval != "255,255,255") {
var hsar = ' hue-rotate(' + hsval[0] + 'deg)';
if (imageURLCId) {
ImURL = imageURLCId;
}
}
}
var bbritef = bright;
if (min_bright > bright) {
bbritef = min_bright;
}
var bbrite = (bbritef / 205);
this.content.innerHTML = `
<!-- Custom Lite Card for x${rgbval}x -->
<img src="${ImURL}" style="filter: opacity(${bbrite})${hsar}!important;" width="100%" height="100%">
`;
} else {
this.content.innerHTML = `
<!-- Custom Lite Card for ${entityId} is turned off -->
`;
}
}
}
setConfig(config) {
if (!config.entity) {
throw new Error('You need to define an entity');
}
this.config = config;
}
// The height of your card. Home Assistant uses this to automatically
// distribute all cards over the available columns.
getCardSize() {
return 3;
}
}
customElements.define('color-lite-card', ColorLite);

View File

@ -0,0 +1,102 @@
class NowPlayingPoster extends HTMLElement {
set hass(hass) {
if (!this.content) {
const card = document.createElement('ha-card');
this.content = document.createElement('div');
//this.content.style = "!important;";
card.appendChild(this.content);
card.style = "background: none;";
this.appendChild(card);
}
const offposter = this.config.off_image;
const entityId = this.config.entity;
const state = hass.states[entityId];
const stateStr = state ? state.state : 'unavailable';
if (state) {
const movposter = state.attributes.entity_picture;
if (["playing", "on"].indexOf(stateStr) > -1 ) {
if ( !movposter ) {
if ( offposter ) {
this.content.innerHTML = `
<!-- now playing card ${entityId} -->
<img src="${offposter}" width=100% align="center" style="">
`;
}
else
{
this.content.innerHTML = `
<!-- now playing card ${entityId} no image-->
`;
}
}
else
{
this.content.innerHTML = `
<!-- now playing card ${entityId} -->
<img src="${movposter}" width=100% height=100%">
`;
}
}
else
{
if ( offposter ) {
this.content.innerHTML = `
<!-- now playing card ${entityId} -->
<img src="${offposter}" width=100% align="center" style="">
`;
}
else
{
this.content.innerHTML = `
<!-- now playing card ${entityId} no image-->
`;
}
}
}
else
{
this.content.innerHTML = `
<!-- now playing card ${entityId} not playing -->
`;
}
}
setConfig(config) {
if (!config.entity) {
throw new Error('You need to define an entity');
}
this.config = config;
}
// The height of your card. Home Assistant uses this to automatically
// distribute all cards over the available columns.
getCardSize() {
return 3;
}
}
customElements.define('now-playing-poster', NowPlayingPoster);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-sun-shiny {
0% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 0.1px 10px;
stroke-dashoffset: -1px;
}
100% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
}
.am-weather-sun-shiny line {
-webkit-animation-name: am-weather-sun-shiny;
-moz-animation-name: am-weather-sun-shiny;
-ms-animation-name: am-weather-sun-shiny;
animation-name: am-weather-sun-shiny;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy-day-1">
<g transform="translate(20,10)">
<g transform="translate(0,16)">
<g class="am-weather-sun">
<g>
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#C6DEFF" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-sun-shiny {
0% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 0.1px 10px;
stroke-dashoffset: -1px;
}
100% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
}
.am-weather-sun-shiny line {
-webkit-animation-name: am-weather-sun-shiny;
-moz-animation-name: am-weather-sun-shiny;
-ms-animation-name: am-weather-sun-shiny;
animation-name: am-weather-sun-shiny;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy-day-2">
<g transform="translate(20,10)">
<g transform="translate(0,16)">
<g class="am-weather-sun">
<g>
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#91C0F8" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-sun-shiny {
0% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 0.1px 10px;
stroke-dashoffset: -1px;
}
100% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
}
.am-weather-sun-shiny line {
-webkit-animation-name: am-weather-sun-shiny;
-moz-animation-name: am-weather-sun-shiny;
-ms-animation-name: am-weather-sun-shiny;
animation-name: am-weather-sun-shiny;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy-day-3">
<g transform="translate(20,10)">
<g transform="translate(0,16)">
<g class="am-weather-sun">
<g>
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** MOON
*/
@keyframes am-weather-moon {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(15deg);
-moz-transform: rotate(15deg);
-ms-transform: rotate(15deg);
transform: rotate(15deg);
}
100% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
}
.am-weather-moon {
-webkit-animation-name: am-weather-moon;
-moz-animation-name: am-weather-moon;
-ms-animation-name: am-weather-moon;
animation-name: am-weather-moon;
-webkit-animation-duration: 6s;
-moz-animation-duration: 6s;
-ms-animation-duration: 6s;
animation-duration: 6s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
}
@keyframes am-weather-moon-star-1 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-1 {
-webkit-animation-name: am-weather-moon-star-1;
-moz-animation-name: am-weather-moon-star-1;
-ms-animation-name: am-weather-moon-star-1;
animation-name: am-weather-moon-star-1;
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
-ms-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
@keyframes am-weather-moon-star-2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-2 {
-webkit-animation-name: am-weather-moon-star-2;
-moz-animation-name: am-weather-moon-star-2;
-ms-animation-name: am-weather-moon-star-2;
animation-name: am-weather-moon-star-2;
-webkit-animation-delay: 5s;
-moz-animation-delay: 5s;
-ms-animation-delay: 5s;
animation-delay: 5s;
-webkit-animation-duration: 4s;
-moz-animation-duration: 4s;
-ms-animation-duration: 4s;
animation-duration: 4s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy-night-1">
<g transform="translate(20,10)">
<g transform="translate(16,4), scale(0.8)">
<g class="am-weather-moon-star-1">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
</g>
<g class="am-weather-moon-star-2">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
</g>
<g class="am-weather-moon">
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
</g>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#C6DEFF" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** MOON
*/
@keyframes am-weather-moon {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(15deg);
-moz-transform: rotate(15deg);
-ms-transform: rotate(15deg);
transform: rotate(15deg);
}
100% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
}
.am-weather-moon {
-webkit-animation-name: am-weather-moon;
-moz-animation-name: am-weather-moon;
-ms-animation-name: am-weather-moon;
animation-name: am-weather-moon;
-webkit-animation-duration: 6s;
-moz-animation-duration: 6s;
-ms-animation-duration: 6s;
animation-duration: 6s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
}
@keyframes am-weather-moon-star-1 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-1 {
-webkit-animation-name: am-weather-moon-star-1;
-moz-animation-name: am-weather-moon-star-1;
-ms-animation-name: am-weather-moon-star-1;
animation-name: am-weather-moon-star-1;
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
-ms-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
@keyframes am-weather-moon-star-2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-2 {
-webkit-animation-name: am-weather-moon-star-2;
-moz-animation-name: am-weather-moon-star-2;
-ms-animation-name: am-weather-moon-star-2;
animation-name: am-weather-moon-star-2;
-webkit-animation-delay: 5s;
-moz-animation-delay: 5s;
-ms-animation-delay: 5s;
animation-delay: 5s;
-webkit-animation-duration: 4s;
-moz-animation-duration: 4s;
-ms-animation-duration: 4s;
animation-duration: 4s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy-night-2">
<g transform="translate(20,10)">
<g transform="translate(16,4), scale(0.8)">
<g class="am-weather-moon-star-1">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
</g>
<g class="am-weather-moon-star-2">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
</g>
<g class="am-weather-moon">
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
</g>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#91C0F8" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,198 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** MOON
*/
@keyframes am-weather-moon {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(15deg);
-moz-transform: rotate(15deg);
-ms-transform: rotate(15deg);
transform: rotate(15deg);
}
100% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
}
.am-weather-moon {
-webkit-animation-name: am-weather-moon;
-moz-animation-name: am-weather-moon;
-ms-animation-name: am-weather-moon;
animation-name: am-weather-moon;
-webkit-animation-duration: 6s;
-moz-animation-duration: 6s;
-ms-animation-duration: 6s;
animation-duration: 6s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
}
@keyframes am-weather-moon-star-1 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-1 {
-webkit-animation-name: am-weather-moon-star-1;
-moz-animation-name: am-weather-moon-star-1;
-ms-animation-name: am-weather-moon-star-1;
animation-name: am-weather-moon-star-1;
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
-ms-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
@keyframes am-weather-moon-star-2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-2 {
-webkit-animation-name: am-weather-moon-star-2;
-moz-animation-name: am-weather-moon-star-2;
-ms-animation-name: am-weather-moon-star-2;
animation-name: am-weather-moon-star-2;
-webkit-animation-delay: 5s;
-moz-animation-delay: 5s;
-ms-animation-delay: 5s;
animation-delay: 5s;
-webkit-animation-duration: 4s;
-moz-animation-duration: 4s;
-ms-animation-duration: 4s;
animation-duration: 4s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy-night-3">
<g transform="translate(20,10)">
<g transform="translate(16,4), scale(0.8)">
<g class="am-weather-moon-star-1">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
</g>
<g class="am-weather-moon-star-2">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
</g>
<g class="am-weather-moon">
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
</g>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,500 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-1 {
0% {
-webkit-transform: translate(-5px,0px);
-moz-transform: translate(-5px,0px);
-ms-transform: translate(-5px,0px);
transform: translate(-5px,0px);
}
50% {
-webkit-transform: translate(10px,0px);
-moz-transform: translate(10px,0px);
-ms-transform: translate(10px,0px);
transform: translate(10px,0px);
}
100% {
-webkit-transform: translate(-5px,0px);
-moz-transform: translate(-5px,0px);
-ms-transform: translate(-5px,0px);
transform: translate(-5px,0px);
}
}
.am-weather-cloud-1 {
-webkit-animation-name: am-weather-cloud-1;
-moz-animation-name: am-weather-cloud-1;
animation-name: am-weather-cloud-1;
-webkit-animation-duration: 7s;
-moz-animation-duration: 7s;
animation-duration: 7s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-sun-shiny {
0% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 0.1px 10px;
stroke-dashoffset: -1px;
}
100% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
}
.am-weather-sun-shiny line {
-webkit-animation-name: am-weather-sun-shiny;
-moz-animation-name: am-weather-sun-shiny;
-ms-animation-name: am-weather-sun-shiny;
animation-name: am-weather-sun-shiny;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** MOON
*/
@keyframes am-weather-moon {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(15deg);
-moz-transform: rotate(15deg);
-ms-transform: rotate(15deg);
transform: rotate(15deg);
}
100% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
}
.am-weather-moon {
-webkit-animation-name: am-weather-moon;
-moz-animation-name: am-weather-moon;
-ms-animation-name: am-weather-moon;
animation-name: am-weather-moon;
-webkit-animation-duration: 6s;
-moz-animation-duration: 6s;
-ms-animation-duration: 6s;
animation-duration: 6s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
}
@keyframes am-weather-moon-star-1 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-1 {
-webkit-animation-name: am-weather-moon-star-1;
-moz-animation-name: am-weather-moon-star-1;
-ms-animation-name: am-weather-moon-star-1;
animation-name: am-weather-moon-star-1;
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
-ms-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
@keyframes am-weather-moon-star-2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-2 {
-webkit-animation-name: am-weather-moon-star-2;
-moz-animation-name: am-weather-moon-star-2;
-ms-animation-name: am-weather-moon-star-2;
animation-name: am-weather-moon-star-2;
-webkit-animation-delay: 5s;
-moz-animation-delay: 5s;
-ms-animation-delay: 5s;
animation-delay: 5s;
-webkit-animation-duration: 4s;
-moz-animation-duration: 4s;
-ms-animation-duration: 4s;
animation-duration: 4s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
/*
** RAIN
*/
@keyframes am-weather-rain {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -100;
}
}
.am-weather-rain-1 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-rain-2 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-delay: 0.25s;
-moz-animation-delay: 0.25s;
-ms-animation-delay: 0.25s;
animation-delay: 0.25s;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SNOW
*/
@keyframes am-weather-snow {
0% {
-webkit-transform: translateX(0) translateY(0);
-moz-transform: translateX(0) translateY(0);
-ms-transform: translateX(0) translateY(0);
transform: translateX(0) translateY(0);
}
33.33% {
-webkit-transform: translateX(-1.2px) translateY(2px);
-moz-transform: translateX(-1.2px) translateY(2px);
-ms-transform: translateX(-1.2px) translateY(2px);
transform: translateX(-1.2px) translateY(2px);
}
66.66% {
-webkit-transform: translateX(1.4px) translateY(4px);
-moz-transform: translateX(1.4px) translateY(4px);
-ms-transform: translateX(1.4px) translateY(4px);
transform: translateX(1.4px) translateY(4px);
opacity: 1;
}
100% {
-webkit-transform: translateX(-1.6px) translateY(6px);
-moz-transform: translateX(-1.6px) translateY(6px);
-ms-transform: translateX(-1.6px) translateY(6px);
transform: translateX(-1.6px) translateY(6px);
opacity: 0;
}
}
@keyframes am-weather-snow-reverse {
0% {
-webkit-transform: translateX(0) translateY(0);
-moz-transform: translateX(0) translateY(0);
-ms-transform: translateX(0) translateY(0);
transform: translateX(0) translateY(0);
}
33.33% {
-webkit-transform: translateX(1.2px) translateY(2px);
-moz-transform: translateX(1.2px) translateY(2px);
-ms-transform: translateX(1.2px) translateY(2px);
transform: translateX(1.2px) translateY(2px);
}
66.66% {
-webkit-transform: translateX(-1.4px) translateY(4px);
-moz-transform: translateX(-1.4px) translateY(4px);
-ms-transform: translateX(-1.4px) translateY(4px);
transform: translateX(-1.4px) translateY(4px);
opacity: 1;
}
100% {
-webkit-transform: translateX(1.6px) translateY(6px);
-moz-transform: translateX(1.6px) translateY(6px);
-ms-transform: translateX(1.6px) translateY(6px);
transform: translateX(1.6px) translateY(6px);
opacity: 0;
}
}
.am-weather-snow-1 {
-webkit-animation-name: am-weather-snow;
-moz-animation-name: am-weather-snow;
-ms-animation-name: am-weather-snow;
animation-name: am-weather-snow;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-snow-2 {
-webkit-animation-name: am-weather-snow;
-moz-animation-name: am-weather-snow;
-ms-animation-name: am-weather-snow;
animation-name: am-weather-snow;
-webkit-animation-delay: 1.2s;
-moz-animation-delay: 1.2s;
-ms-animation-delay: 1.2s;
animation-delay: 1.2s;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-snow-3 {
-webkit-animation-name: am-weather-snow-reverse;
-moz-animation-name: am-weather-snow-reverse;
-ms-animation-name: am-weather-snow-reverse;
animation-name: am-weather-snow-reverse;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** EASING
*/
.am-weather-easing-ease-in-out {
-webkit-animation-timing-function: ease-in-out;
-moz-animation-timing-function: ease-in-out;
-ms-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
}
]]></style>
</defs>
<g filter="url(#blur)" id="cloudy">
<g transform="translate(20,10)">
<g class="am-weather-cloud-1">
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#91C0F8" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-10,-8), scale(0.6)"/>
</g>
<g class="am-weather-cloud-2">
<path d="M47.7,35.4 c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,521 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-1 {
0% {
-webkit-transform: translate(-5px,0px);
-moz-transform: translate(-5px,0px);
-ms-transform: translate(-5px,0px);
transform: translate(-5px,0px);
}
50% {
-webkit-transform: translate(10px,0px);
-moz-transform: translate(10px,0px);
-ms-transform: translate(10px,0px);
transform: translate(10px,0px);
}
100% {
-webkit-transform: translate(-5px,0px);
-moz-transform: translate(-5px,0px);
-ms-transform: translate(-5px,0px);
transform: translate(-5px,0px);
}
}
.am-weather-cloud-1 {
-webkit-animation-name: am-weather-cloud-1;
-moz-animation-name: am-weather-cloud-1;
animation-name: am-weather-cloud-1;
-webkit-animation-duration: 7s;
-moz-animation-duration: 7s;
animation-duration: 7s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-sun-shiny {
0% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 0.1px 10px;
stroke-dashoffset: -1px;
}
100% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
}
.am-weather-sun-shiny line {
-webkit-animation-name: am-weather-sun-shiny;
-moz-animation-name: am-weather-sun-shiny;
-ms-animation-name: am-weather-sun-shiny;
animation-name: am-weather-sun-shiny;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** MOON
*/
@keyframes am-weather-moon {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(15deg);
-moz-transform: rotate(15deg);
-ms-transform: rotate(15deg);
transform: rotate(15deg);
}
100% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
}
.am-weather-moon {
-webkit-animation-name: am-weather-moon;
-moz-animation-name: am-weather-moon;
-ms-animation-name: am-weather-moon;
animation-name: am-weather-moon;
-webkit-animation-duration: 6s;
-moz-animation-duration: 6s;
-ms-animation-duration: 6s;
animation-duration: 6s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
}
@keyframes am-weather-moon-star-1 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-1 {
-webkit-animation-name: am-weather-moon-star-1;
-moz-animation-name: am-weather-moon-star-1;
-ms-animation-name: am-weather-moon-star-1;
animation-name: am-weather-moon-star-1;
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
-ms-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
@keyframes am-weather-moon-star-2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-2 {
-webkit-animation-name: am-weather-moon-star-2;
-moz-animation-name: am-weather-moon-star-2;
-ms-animation-name: am-weather-moon-star-2;
animation-name: am-weather-moon-star-2;
-webkit-animation-delay: 5s;
-moz-animation-delay: 5s;
-ms-animation-delay: 5s;
animation-delay: 5s;
-webkit-animation-duration: 4s;
-moz-animation-duration: 4s;
-ms-animation-duration: 4s;
animation-duration: 4s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
/*
** RAIN
*/
@keyframes am-weather-rain {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -100;
}
}
.am-weather-rain-1 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-rain-2 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-delay: 0.25s;
-moz-animation-delay: 0.25s;
-ms-animation-delay: 0.25s;
animation-delay: 0.25s;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SNOW
*/
@keyframes am-weather-snow {
0% {
-webkit-transform: translateX(0) translateY(0);
-moz-transform: translateX(0) translateY(0);
-ms-transform: translateX(0) translateY(0);
transform: translateX(0) translateY(0);
}
33.33% {
-webkit-transform: translateX(-1.2px) translateY(2px);
-moz-transform: translateX(-1.2px) translateY(2px);
-ms-transform: translateX(-1.2px) translateY(2px);
transform: translateX(-1.2px) translateY(2px);
}
66.66% {
-webkit-transform: translateX(1.4px) translateY(4px);
-moz-transform: translateX(1.4px) translateY(4px);
-ms-transform: translateX(1.4px) translateY(4px);
transform: translateX(1.4px) translateY(4px);
opacity: 1;
}
100% {
-webkit-transform: translateX(-1.6px) translateY(6px);
-moz-transform: translateX(-1.6px) translateY(6px);
-ms-transform: translateX(-1.6px) translateY(6px);
transform: translateX(-1.6px) translateY(6px);
opacity: 0;
}
}
@keyframes am-weather-snow-reverse {
0% {
-webkit-transform: translateX(0) translateY(0);
-moz-transform: translateX(0) translateY(0);
-ms-transform: translateX(0) translateY(0);
transform: translateX(0) translateY(0);
}
33.33% {
-webkit-transform: translateX(1.2px) translateY(2px);
-moz-transform: translateX(1.2px) translateY(2px);
-ms-transform: translateX(1.2px) translateY(2px);
transform: translateX(1.2px) translateY(2px);
}
66.66% {
-webkit-transform: translateX(-1.4px) translateY(4px);
-moz-transform: translateX(-1.4px) translateY(4px);
-ms-transform: translateX(-1.4px) translateY(4px);
transform: translateX(-1.4px) translateY(4px);
opacity: 1;
}
100% {
-webkit-transform: translateX(1.6px) translateY(6px);
-moz-transform: translateX(1.6px) translateY(6px);
-ms-transform: translateX(1.6px) translateY(6px);
transform: translateX(1.6px) translateY(6px);
opacity: 0;
}
}
.am-weather-snow-1 {
-webkit-animation-name: am-weather-snow;
-moz-animation-name: am-weather-snow;
-ms-animation-name: am-weather-snow;
animation-name: am-weather-snow;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-snow-2 {
-webkit-animation-name: am-weather-snow;
-moz-animation-name: am-weather-snow;
-ms-animation-name: am-weather-snow;
animation-name: am-weather-snow;
-webkit-animation-delay: 1.2s;
-moz-animation-delay: 1.2s;
-ms-animation-delay: 1.2s;
animation-delay: 1.2s;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-snow-3 {
-webkit-animation-name: am-weather-snow-reverse;
-moz-animation-name: am-weather-snow-reverse;
-ms-animation-name: am-weather-snow-reverse;
animation-name: am-weather-snow-reverse;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** EASING
*/
.am-weather-easing-ease-in-out {
-webkit-animation-timing-function: ease-in-out;
-moz-animation-timing-function: ease-in-out;
-ms-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
}
]]></style>
</defs>
<g filter="url(#blur)" id="day">
<g transform="translate(32,32)">
<g class="am-weather-sun am-weather-sun-shiny am-weather-easing-ease-in-out">
<g>
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3" />
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,503 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** CLOUDS
*/
@keyframes am-weather-cloud-1 {
0% {
-webkit-transform: translate(-5px,0px);
-moz-transform: translate(-5px,0px);
-ms-transform: translate(-5px,0px);
transform: translate(-5px,0px);
}
50% {
-webkit-transform: translate(10px,0px);
-moz-transform: translate(10px,0px);
-ms-transform: translate(10px,0px);
transform: translate(10px,0px);
}
100% {
-webkit-transform: translate(-5px,0px);
-moz-transform: translate(-5px,0px);
-ms-transform: translate(-5px,0px);
transform: translate(-5px,0px);
}
}
.am-weather-cloud-1 {
-webkit-animation-name: am-weather-cloud-1;
-moz-animation-name: am-weather-cloud-1;
animation-name: am-weather-cloud-1;
-webkit-animation-duration: 7s;
-moz-animation-duration: 7s;
animation-duration: 7s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-cloud-2 {
0% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
50% {
-webkit-transform: translate(2px,0px);
-moz-transform: translate(2px,0px);
-ms-transform: translate(2px,0px);
transform: translate(2px,0px);
}
100% {
-webkit-transform: translate(0px,0px);
-moz-transform: translate(0px,0px);
-ms-transform: translate(0px,0px);
transform: translate(0px,0px);
}
}
.am-weather-cloud-2 {
-webkit-animation-name: am-weather-cloud-2;
-moz-animation-name: am-weather-cloud-2;
animation-name: am-weather-cloud-2;
-webkit-animation-duration: 3s;
-moz-animation-duration: 3s;
animation-duration: 3s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
@keyframes am-weather-sun-shiny {
0% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
50% {
stroke-dasharray: 0.1px 10px;
stroke-dashoffset: -1px;
}
100% {
stroke-dasharray: 3px 10px;
stroke-dashoffset: 0px;
}
}
.am-weather-sun-shiny line {
-webkit-animation-name: am-weather-sun-shiny;
-moz-animation-name: am-weather-sun-shiny;
-ms-animation-name: am-weather-sun-shiny;
animation-name: am-weather-sun-shiny;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** MOON
*/
@keyframes am-weather-moon {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
50% {
-webkit-transform: rotate(15deg);
-moz-transform: rotate(15deg);
-ms-transform: rotate(15deg);
transform: rotate(15deg);
}
100% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
}
.am-weather-moon {
-webkit-animation-name: am-weather-moon;
-moz-animation-name: am-weather-moon;
-ms-animation-name: am-weather-moon;
animation-name: am-weather-moon;
-webkit-animation-duration: 6s;
-moz-animation-duration: 6s;
-ms-animation-duration: 6s;
animation-duration: 6s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-moz-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
-ms-transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
transform-origin: 12.5px 15.15px 0; /* TODO FF CENTER ISSUE */
}
@keyframes am-weather-moon-star-1 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-1 {
-webkit-animation-name: am-weather-moon-star-1;
-moz-animation-name: am-weather-moon-star-1;
-ms-animation-name: am-weather-moon-star-1;
animation-name: am-weather-moon-star-1;
-webkit-animation-delay: 3s;
-moz-animation-delay: 3s;
-ms-animation-delay: 3s;
animation-delay: 3s;
-webkit-animation-duration: 5s;
-moz-animation-duration: 5s;
-ms-animation-duration: 5s;
animation-duration: 5s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
@keyframes am-weather-moon-star-2 {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.am-weather-moon-star-2 {
-webkit-animation-name: am-weather-moon-star-2;
-moz-animation-name: am-weather-moon-star-2;
-ms-animation-name: am-weather-moon-star-2;
animation-name: am-weather-moon-star-2;
-webkit-animation-delay: 5s;
-moz-animation-delay: 5s;
-ms-animation-delay: 5s;
animation-delay: 5s;
-webkit-animation-duration: 4s;
-moz-animation-duration: 4s;
-ms-animation-duration: 4s;
animation-duration: 4s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: 1;
-moz-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
animation-iteration-count: 1;
}
/*
** RAIN
*/
@keyframes am-weather-rain {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -100;
}
}
.am-weather-rain-1 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-rain-2 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-delay: 0.25s;
-moz-animation-delay: 0.25s;
-ms-animation-delay: 0.25s;
animation-delay: 0.25s;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** SNOW
*/
@keyframes am-weather-snow {
0% {
-webkit-transform: translateX(0) translateY(0);
-moz-transform: translateX(0) translateY(0);
-ms-transform: translateX(0) translateY(0);
transform: translateX(0) translateY(0);
}
33.33% {
-webkit-transform: translateX(-1.2px) translateY(2px);
-moz-transform: translateX(-1.2px) translateY(2px);
-ms-transform: translateX(-1.2px) translateY(2px);
transform: translateX(-1.2px) translateY(2px);
}
66.66% {
-webkit-transform: translateX(1.4px) translateY(4px);
-moz-transform: translateX(1.4px) translateY(4px);
-ms-transform: translateX(1.4px) translateY(4px);
transform: translateX(1.4px) translateY(4px);
opacity: 1;
}
100% {
-webkit-transform: translateX(-1.6px) translateY(6px);
-moz-transform: translateX(-1.6px) translateY(6px);
-ms-transform: translateX(-1.6px) translateY(6px);
transform: translateX(-1.6px) translateY(6px);
opacity: 0;
}
}
@keyframes am-weather-snow-reverse {
0% {
-webkit-transform: translateX(0) translateY(0);
-moz-transform: translateX(0) translateY(0);
-ms-transform: translateX(0) translateY(0);
transform: translateX(0) translateY(0);
}
33.33% {
-webkit-transform: translateX(1.2px) translateY(2px);
-moz-transform: translateX(1.2px) translateY(2px);
-ms-transform: translateX(1.2px) translateY(2px);
transform: translateX(1.2px) translateY(2px);
}
66.66% {
-webkit-transform: translateX(-1.4px) translateY(4px);
-moz-transform: translateX(-1.4px) translateY(4px);
-ms-transform: translateX(-1.4px) translateY(4px);
transform: translateX(-1.4px) translateY(4px);
opacity: 1;
}
100% {
-webkit-transform: translateX(1.6px) translateY(6px);
-moz-transform: translateX(1.6px) translateY(6px);
-ms-transform: translateX(1.6px) translateY(6px);
transform: translateX(1.6px) translateY(6px);
opacity: 0;
}
}
.am-weather-snow-1 {
-webkit-animation-name: am-weather-snow;
-moz-animation-name: am-weather-snow;
-ms-animation-name: am-weather-snow;
animation-name: am-weather-snow;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-snow-2 {
-webkit-animation-name: am-weather-snow;
-moz-animation-name: am-weather-snow;
-ms-animation-name: am-weather-snow;
animation-name: am-weather-snow;
-webkit-animation-delay: 1.2s;
-moz-animation-delay: 1.2s;
-ms-animation-delay: 1.2s;
animation-delay: 1.2s;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-snow-3 {
-webkit-animation-name: am-weather-snow-reverse;
-moz-animation-name: am-weather-snow-reverse;
-ms-animation-name: am-weather-snow-reverse;
animation-name: am-weather-snow-reverse;
-webkit-animation-duration: 2s;
-moz-animation-duration: 2s;
-ms-animation-duration: 2s;
animation-duration: 2s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** EASING
*/
.am-weather-easing-ease-in-out {
-webkit-animation-timing-function: ease-in-out;
-moz-animation-timing-function: ease-in-out;
-ms-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out;
}
]]></style>
</defs>
<g filter="url(#blur)" id="night">
<g transform="translate(20,20)">
<g class="am-weather-moon-star-1">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10"/>
</g>
<g class="am-weather-moon-star-2">
<polygon fill="orange" points="3.3,1.5 4,2.7 5.2,3.3 4,4 3.3,5.2 2.7,4 1.5,3.3 2.7,2.7" stroke="none" stroke-miterlimit="10" transform="translate(20,10)"/>
</g>
<g class="am-weather-moon">
<path d="M14.5,13.2c0-3.7,2-6.9,5-8.7 c-1.5-0.9-3.2-1.3-5-1.3c-5.5,0-10,4.5-10,10s4.5,10,10,10c1.8,0,3.5-0.5,5-1.3C16.5,20.2,14.5,16.9,14.5,13.2z" fill="orange" stroke="orange" stroke-linejoin="round" stroke-width="2"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** RAIN
*/
@keyframes am-weather-rain {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -100;
}
}
.am-weather-rain-1 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-rain-2 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-delay: 0.25s;
-moz-animation-delay: 0.25s;
-ms-animation-delay: 0.25s;
animation-delay: 0.25s;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
]]></style>
</defs>
<g filter="url(#blur)" id="rainy-1">
<g transform="translate(20,10)">
<g transform="translate(0,16), scale(1.2)">
<g class="am-weather-sun">
<g>
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
<g>
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.5" transform="translate(-15,-5), scale(0.85)"/>
</g>
</g>
<g transform="translate(34,46), rotate(10)">
<line class="am-weather-rain-1" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(-6,1)" x1="0" x2="0" y1="0" y2="8" />
<line class="am-weather-rain-2" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(0,-1)" x1="0" x2="0" y1="0" y2="8" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** RAIN
*/
@keyframes am-weather-rain {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -100;
}
}
.am-weather-rain-1 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
]]></style>
</defs>
<g filter="url(#blur)" id="rainy-2">
<g transform="translate(20,10)">
<g transform="translate(0,16)">
<g class="am-weather-sun">
<g>
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
<g>
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
<g transform="translate(37,45), rotate(10)">
<line class="am-weather-rain-1" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(-6,1)" x1="0" x2="0" y1="0" y2="8" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- (c) ammap.com | SVG weather icons -->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="64"
height="64"
viewbox="0 0 64 64">
<defs>
<filter id="blur" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="3"/>
<feOffset dx="0" dy="4" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.05"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style type="text/css"><![CDATA[
/*
** SUN
*/
@keyframes am-weather-sun {
0% {
-webkit-transform: rotate(0deg);
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
.am-weather-sun {
-webkit-animation-name: am-weather-sun;
-moz-animation-name: am-weather-sun;
-ms-animation-name: am-weather-sun;
animation-name: am-weather-sun;
-webkit-animation-duration: 9s;
-moz-animation-duration: 9s;
-ms-animation-duration: 9s;
animation-duration: 9s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
/*
** RAIN
*/
@keyframes am-weather-rain {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -100;
}
}
.am-weather-rain-1 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.am-weather-rain-2 {
-webkit-animation-name: am-weather-rain;
-moz-animation-name: am-weather-rain;
-ms-animation-name: am-weather-rain;
animation-name: am-weather-rain;
-webkit-animation-delay: 0.25s;
-moz-animation-delay: 0.25s;
-ms-animation-delay: 0.25s;
animation-delay: 0.25s;
-webkit-animation-duration: 8s;
-moz-animation-duration: 8s;
-ms-animation-duration: 8s;
animation-duration: 8s;
-webkit-animation-timing-function: linear;
-moz-animation-timing-function: linear;
-ms-animation-timing-function: linear;
animation-timing-function: linear;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
-ms-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
]]></style>
</defs>
<g filter="url(#blur)" id="rainy-3">
<g transform="translate(20,10)">
<g transform="translate(0,16)">
<g class="am-weather-sun">
<g>
<line fifll="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(45)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(90)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(135)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(180)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(225)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(270)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
<g transform="rotate(315)">
<line fill="none" stroke="orange" stroke-linecap="round" stroke-width="2" transform="translate(0,9)" x1="0" x2="0" y1="0" y2="3"/>
</g>
</g>
<circle cx="0" cy="0" fill="orange" r="5" stroke="orange" stroke-width="2"/>
</g>
<g>
<path d="M47.7,35.4c0-4.6-3.7-8.2-8.2-8.2c-1,0-1.9,0.2-2.8,0.5c-0.3-3.4-3.1-6.2-6.6-6.2c-3.7,0-6.7,3-6.7,6.7c0,0.8,0.2,1.6,0.4,2.3 c-0.3-0.1-0.7-0.1-1-0.1c-3.7,0-6.7,3-6.7,6.7c0,3.6,2.9,6.6,6.5,6.7l17.2,0C44.2,43.3,47.7,39.8,47.7,35.4z" fill="#57A0EE" stroke="white" stroke-linejoin="round" stroke-width="1.2" transform="translate(-20,-11)"/>
</g>
</g>
<g transform="translate(34,46), rotate(10)">
<line class="am-weather-rain-1" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(-6,1)" x1="0" x2="0" y1="0" y2="8" />
<line class="am-weather-rain-2" fill="none" stroke="#91C0F8" stroke-dasharray="4,7" stroke-linecap="round" stroke-width="2" transform="translate(0,-1)" x1="0" x2="0" y1="0" y2="8" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

Some files were not shown because too many files have changed in this diff Show More