├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── README.md
├── README.md.old
├── alarm.yaml
├── automation.yaml
├── automation
└── panic_mode.yaml
├── custom_components
└── alarm_control_panel
│ └── bwalarm.py
├── example automations
└── panic_mode.yaml
├── guidance
└── configuration.md
├── historic_changelog.md
├── panel_custom.yaml
├── panels
└── alarm.html
└── www
├── alarm
├── alarm.css
├── custom-element.html
├── donate
│ ├── bch.png
│ ├── btc.png
│ ├── eth.png
│ ├── ltc.png
│ ├── paypal.png
│ └── xrp.png
└── lobster.woff2
├── images
├── camera-garage.jpg
├── camera-outdoor.jpg
├── camera-pool.jpg
├── camera-tv-room.jpg
├── gazos.jpg
└── ha.png
└── lib
├── countdown360.js
├── jquery-3.2.1.min.js
├── jscolor.js
└── sha256.js
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Browser (please complete the following information):**
24 | - Browser [e.g. chrome, safari]
25 | - Version [e.g. 22]
26 |
27 | **Additional context**
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Home Assistant - Custom Alarm Interface!
2 | ## Intro :-)
3 |
4 | Welcome my fellow modders, tinkerers, home assistant wizards!!
5 |
6 | Follow the thread [here](https://community.home-assistant.io/t/yet-another-take-on-an-alarm-system/32386)
7 |
8 | Consider donating to this project to keep it going as anything contributed will be placed back in to enable more hardware integration, new features and bug squashing.
9 |
10 | This is very much a community project so if you wish to chip in then please do!! I could really use a CSS, animation, design guru to make this look amazing. Also please feel free to leave comments, suggestions, enhancements and fixes!!
11 |
12 | **NOTE!!! MAJOR CHANGE** It's time to publish the New UI and settings into the master release.
13 |
14 | ## Installation
15 |
16 | You will need to copy the following files into your home assistant configuration directory
17 |
18 | alarm.yaml *This files stores your alarm configuration. An options page will be created for this file*
19 | custom_components/alarm_control_panel/bwalarm.py *The brains of the operation. This is the logic of the custom alarm system*
20 | panels/alarm.html *This is the interface for the custom alarm component. It's actually optional as the alarm will function without it but recommended for ease of setup*
21 | www/alarm/[ALL FILES] *These files control how the interface looks and feels*
22 | www/lib/[ALL FILES] *These files add additional functionality to the interface in order to work*
23 | www/images/ha.png *An image file used for the interface log*
24 |
25 | To get things working with Home Assistant (HA) you will need to adjust your configuration.yaml to instruct HA to use your new custom alarm component, add the following to this file:
26 | ```
27 | alarm_control_panel: !include alarm.yaml
28 | ```
29 | You will also need to tell HA where your new panel interface file is. Also add the following to your configuration.yaml:
30 | ```
31 | panel_custom: !include panel_custom.yaml
32 | ```
33 | You may need to restart HA if the component doesn't load first time as HA will need to install a dependency (ruamel.yaml).
34 |
35 | It's advisable to start with a new alarm.yaml file with the minimum configuration set:
36 | ```
37 | platform: bwalarm
38 | name: House
39 | ```
40 | Your new interface can be used to modify your alarm.yaml directly.
41 |
42 | The default password to access the settings page is: **HG28!!&dn**
43 |
44 | Please test and provide feedback/suggestions.
45 |
46 | ### Features:
47 | - State specific groups and times (NEW)
48 | - User specific codes
49 | - Panic Mode
50 | - MQTT Integration
51 | - Floorplan Integration
52 | - Alarm State Persistence on reboots/power restore
53 | - Lockout of HA sidebar when armed
54 | - Custom Panel allowing your own html to display whatever you choose (Cameras, Sliding Images etc)
55 | - Passcode Attemps/Lockout
56 | - Support for custom device states
57 | - Code panel 0-9 on disarm only
58 | - Weather Status (Optional) - **NOTE:** Weather sensor nows supports generic sensors (sensor.weather_summary & sensor.weather_temperature) if these are not found then it will default to the dark sky sensors (sensor.dark_sky_summary & sensor.dark_sky_temperature)
59 | - Perimeter Mode (Optional) - I use this to only arm a particular set of sensors (doors) whilst I'm using all floors.
60 | - Masks passcode on entry
61 | - clock display (Optional)
62 | - Digit code entry on disarm
63 | - Themed colours depending on alarm state
64 | - Countdown timer on 'Pending' state
65 | - Notification of Open Sensors with the option to override
66 | - Information/Debug panel
67 |
68 | ### Testing
69 | - Tested on HA v0.87 and below.
70 |
71 | ### Change Log:
72 | - 10/02/19:
73 | - [REQUEST] Added option to hide sensors when alarm armed
74 | - [REQUEST] Added option to display in fahrenheit
75 | - [FIX] Panel lockout display
76 | - [FIX] HA 0.87 compatibility
77 | - [REQUEST] Included switches into the sensor lists as requested
78 | - [ENHANCEMENT] Modified the layouts due to polymer changes
79 |
80 | - 27/11/18:
81 | - [FEATURE] Adding some basic error handling which will be enhanced at a later date
82 | - [FIX BUG] Fixed margin issue in firefox (settings)
83 | - [REQUEST] Sorted sensors alphabetically
84 | - [FIX BUG] Fixed clock, serif, weather, passcode display issues
85 |
86 | - 22/11/18:
87 | - Quite a few bugs and issues have been resolved on this release. There has also been a number of changes to the config file layout so you are likely required to start from scratch as the users, themes and panel settings have changed.
88 |
89 | - Updated alarm.html to 1.3.3
90 | - Updated bwalarm.py to 1.1.3
91 |
92 | - fixed duplicate sensors in settings panel
93 | - fixed passcode attempts setting
94 | - fixed code to arm display issues
95 | - fixed persistant mode
96 | - fixed sesnor groups
97 | - fixed code to arm panel display and alignment
98 | - reformated logs
99 | - fixed log (displaying name and image)
100 | - removed windows line feed
101 | - integrated HASS users into alarm automatically however these initially are disabled
102 | - fixed switch breaks on service call
103 | - fixed themes
104 |
105 | ## Note!
106 | Beware, here be dragons! There may be bugs, issues whilst I get this off the ground and there will definately be design problems when used with different size browsers etc. Hopefully we can conquer these in due course!..
107 |
108 | ## Thanks!
109 | Thanks to the community for all the input into this.
110 |
111 | Consider supporting this project and donate! All funds will go towards bringing new features, hardware support and bug squashing!!
112 |
113 | - BTC Address: 1NFeyzpKKiKbBYSmCLQZQLxBqJbhSbqmwd
114 | - LTC Address: LTUViN3QUESkQk3mG2hvTzhLRQPVAd269f
115 | - XRP Address: rwuMp76ht6dmGvipxwKr5ZE6VpF7ZKC7qs
116 | - ETH Address: 0xCbeD2D2cf0434370c1ca126707009b876b736609
117 | - Paypal: ha.custom.alarm@gmail.com
118 |
119 | ## Credits
120 | [A great countdown JS that I have slightly modded](https://github.com/johnschult/jquery.countdown360)
121 |
--------------------------------------------------------------------------------
/README.md.old:
--------------------------------------------------------------------------------
1 | # Home Assistant - Custom Alarm Interface!
2 | ## Intro :-)
3 |
4 |
5 | Welcome my fellow modders, tinkerers, home assistant wizards!!
6 |
7 | Follow the thread [here](https://community.home-assistant.io/t/yet-another-take-on-an-alarm-system/32386)
8 |
9 | Consider donating to this project to keep it going as anything contributed will be placed back in to enable more hardware integration, new features and bug squashing.
10 |
11 | This is very much a community project so if you wish to chip in then please do!! I could really use a CSS, animation, design guru to make this look amazing. Also please feel free to leave comments, suggestions, enhancements and fixes!!
12 |
13 | ### Features:
14 | - Multi Language Support (NEW)
15 | - State specific groups and times (NEW)
16 | - Panic Mode
17 | - MQTT Integration
18 | - Alarm State Persistence on reboots/power restore
19 | - Lockout of HA sidebar when armed
20 | - Custom Panel allowing your own html to display whatever you choose (Cameras, Sliding Images etc)
21 | - Passcode Attemps/Lockout
22 | - Support for custom device states
23 | - Code panel 0-9 on disarm only
24 | - Weather Status (Optional) - **NOTE:** Weather sensor nows supports generic sensors (sensor.weather_summary & sensor.weather_temperature) if these are not found then it will deault to the dark sky sensors (sensor.dark_sky_summary & sensor.dark_sky_temperature)
25 | - Perimeter Mode (Optional) - I use this to only arm a particular set of sensors (doors) whilst im using all floors.
26 | - Masks passcode on entry
27 | - clock display (Optional)
28 | - Digit code entry on disarm
29 | - Themed colours depending on alarm state
30 | - Countdown timer on 'Pending' state
31 | - Notification of Open Sensors with the option to override
32 | - Information/Debug panel
33 |
34 | ### To be implemented:
35 | - Settings page to adjust non-critical features (colours/information)
36 | - Information/Debug Mode to be enhanced
37 | - Screensaver
38 | - Customisable Themes
39 | - Time Based themes (Dark at Night - Light during day)
40 | - Possibly a full black one with a Cylon style bar when activated?
41 | - Please submit some ideas here
42 | - Guest mode / reduced feature set
43 | - Clean up of code (html/css/python)
44 | - Anything anyone else can think of?
45 |
46 | [Installation/Configuration Instructions](guidance/configuration.md)
47 |
48 | ### Testing
49 | - Tested on HA v0.65.5 and below.
50 |
51 | ### Recent Changelog
52 | - (26/03/18) FEATURE - Multi Language Support!!! English and Portuguese options are available out of the box however you can add your own translations by adding in your own translation file. SO technically you could translated the English GUI to say Esperanto! English is the default. This options is set in your alarm.yaml (language: 'portuguese'). Setting this option prompts the front end panel to look for a json styled translation file with the name of your chosen language saved in www/alarm/language/[your language].json The file must exist and use the json 'style' formatted as seen in the example portuguese file. Each english word in the panel will be translated into a matched translated word. See the portuguese json file as an example.
53 |
54 | - (25/03/18) BUG FIX - Moved comments line above the actual config to resolve the hassio issues
55 | - (25/03/18) BUG FIX - Fix to resolve slidebar constantly opening when using mobile devices (Panel 1.0.1 / Bwalarm 1.0.1)
56 |
57 | - (24/03/18) A Massive Thanks to those that have donated!!! IT is very much appreciated and helps to keep this project alive. Also keep the suggestions flowing and lets make this the best alarm system ever!!!!!!!!!!!!!!!
58 | - (24/03/18) MAJOR UPDATE! - State specific groups/times. Each state must! configure it's own groups. Home and Away are mandatory with Perimeter mode optional. The top level groups have been dropped so you will need to remove these from your alarm.yaml. You will need to update your alarm.yaml!. The ignore/notathome groups have been dropped from the setup. Please see the default alarm.yaml to inform your own setup. An example of the configuration below (if you get stuck then post an issue or ask in the forum):
59 | ```
60 | armed_home: #Either home/away with perimeter as optional
61 | pending_time: 10 #[OPTIONAL] State specific overrides default time
62 | trigger_time: 600 #[OPTIONAL] State specific overrides default time
63 | immediate: #[OPTIONAL however either an immediate or delayed group must exist]
64 | - binary_sensor.whatever
65 | delayed: #[OPTIONAL]
66 | - binary_sensor.whatever
67 | override: #[OPTIONAL]
68 | - binary_sensor.whatever
69 | ```
70 | - (24/03/18) FEATURE - Added an information button in the bottom right of the panel which shows any detected errors and version information for debugging, needs a little finesse
71 | - (24/03/18) UPDATE - Weather sensor nows supports generic sensors (sensor.weather_summary & sensor.weather_temperature) if these are not found then it will deault to the dark sky sensors (sensor.dark_sky_summary & sensor.dark_sky_temperature)
72 | - (24/03/18) UPDATE - Code cleanup in alarm.html
73 | - (24/03/18) BUG FIX - Removed the need for alarm_script.js (this may re-appear in a later release if we need extra js code) as the hide sidebar feature now natively supports HA close/open sidebar rather than a javascript hack.
74 |
75 | - (14/03/18) BUG FIX - UI fix on the sensor groups moving all active sensors into the immediate group when no pending time is set for that particular state.
76 | - (14/03/18) BUG FIX - Custom pending times now accurate set the countdown clock in the panel UI
77 | - (14/03/18) UPDATE - Perimeter Colours added to customisation
78 |
79 | [Historic Changelog](historic_changelog.md)
80 |
81 | ## Note!
82 | Beware, here be dragons! There may be bugs, issues whilst I get this off the ground and there will definately be design problems when used with different size browsers etc. Hopefully we can conquer these in due course!..
83 |
84 | ## Thanks!
85 | Thanks to the community for all the input into this.
86 |
87 | Consider supporting this project and donate! All funds will go towards bringing new features, hardware support and bug squashing!!
88 |
89 | - BTC Address: 1NFeyzpKKiKbBYSmCLQZQLxBqJbhSbqmwd
90 | - LTC Address: LTUViN3QUESkQk3mG2hvTzhLRQPVAd269f
91 | - XRP Address: rwuMp76ht6dmGvipxwKr5ZE6VpF7ZKC7qs
92 | - ETH Address: 0xCbeD2D2cf0434370c1ca126707009b876b736609
93 | - Paypal: ha.custom.alarm@gmail.com
94 |
95 | ## Credits
96 | [A great countdown JS that I have slightly modded](https://github.com/johnschult/jquery.countdown360)
97 |
--------------------------------------------------------------------------------
/alarm.yaml:
--------------------------------------------------------------------------------
1 | ##########################################################
2 | ## CUSTOM ALARM COMPONENT ALARM.YAML
3 | ## https://github.com/gazoscalvertos/Hass-Custom-Alarm
4 | ## VERSION: 1.0.2
5 | ## MODIFIED: 18/04/18
6 | ## CHANGE LOG:
7 | ## Add Multi Codes, names, pics
8 | ## optional code to arm alarm
9 | ## Default Interface password: HG28!!&dn
10 | ##########################################################
11 |
12 | platform: bwalarm
13 | name: House
14 | panel:
15 | camera_update_interval: ''
16 | cameras: []
17 | enable_camera_panel: 'False'
18 | enable_clock: 'True'
19 | enable_clock_12hr: 'True'
20 | enable_custom_panel: 'False'
21 | enable_floorplan_panel: 'False'
22 | enable_sensors_panel: 'True'
23 | enable_serif_font: 'True'
24 | enable_weather: 'True'
25 | hide_passcode: 'True'
26 | panel_title: Surname Residence
27 | shadow_effect: 'True'
28 | enable_fahrenheit: false
29 | states:
30 | armed_away:
31 | immediate:
32 | - switch.skylight
33 | - binary_sensor.away_sensor
34 | delayed:
35 | - binary_sensor.away_delayed_sensor
36 | override: []
37 | pending_time: 2
38 | warning_time: 2
39 | trigger_time: 5
40 | armed_home:
41 | immediate:
42 | - binary_sensor.home_sensor
43 | delayed:
44 | - binary_sensor.home_delayed_sensor
45 | override: []
46 | pending_time: 2
47 | warning_time: 2
48 | trigger_time: 5
49 | armed_perimeter:
50 | immediate:
51 | - binary_sensor.perimeter_sensor
52 | delayed:
53 | - binary_sensor.perimeter_delayed_sensor
54 | override: []
55 | pending_time: 0
56 | warning_time: '2'
57 | trigger_time: 600
58 | users:
59 | - id: 85a1f1f7b2f247dfafe718d0cfe5026d
60 | name: test
61 | enabled: false
62 | code: 85a1f1f7b2f247dfafe718d0cfe5026d
63 | picture: /local/images/ha.png
64 | - id: 36ad4844cafe4f3491e8e4e2ac38e0ff
65 | name: bart
66 | enabled: true
67 | code: '2345'
68 | picture: /local/images/hal.png
69 | - id: 2820bcde9f974ef18da3b44b1e494cf3
70 | name: Legacy API password user
71 | enabled: false
72 | code: 2820bcde9f974ef18da3b44b1e494cf3
73 | picture: /local/images/ha.png
74 | - id: 9b229b1e281643409c56bcfbb7e0088a
75 | name: Legacy API password user
76 | enabled: false
77 | code: 9b229b1e281643409c56bcfbb7e0088a
78 | picture: /local/images/ha.png
79 | admin_password: a
80 | code: '1234'
81 | enable_perimeter_mode: true
82 | code_to_arm: false
83 | themes:
84 | - name: aaa
85 | warning_color: '#995BFF'
86 | pending_color: '#FF2943'
87 | disarmed_color: '#FF22E6'
88 | triggered_color: '#FF0000'
89 | armed_home_color: '#C1B1FF'
90 | armed_away_color: '#FF8686'
91 | armed_perimeter_color: '#DAFF9E'
92 | active: false
93 | action_button_border_color: '#3ED5FF'
94 | panic_code: '9876'
95 | enable_log: true
96 | passcode_attempts: '2'
97 | passcode_attempts_timeout: '10'
98 |
--------------------------------------------------------------------------------
/automation.yaml:
--------------------------------------------------------------------------------
1 | - id: alarm_armed_away
2 | alias: '[Alarm] Away Mode Armed'
3 | trigger:
4 | - platform: state
5 | entity_id: alarm_control_panel.house
6 | to: 'armed_away'
7 | action:
8 | - data:
9 | message: 'Alarm Away Mode Armed'
10 | target: email/example@gmail.com
11 | service: notify.pushbullet
12 | - data:
13 | message: 'The house alarm has been switched on in away mode. Goodbye'
14 | service: notify.example_phone_tts
15 |
16 | - id: alarm_armed_home
17 | alias: '[Alarm] Home Mode Armed'
18 | trigger:
19 | - platform: state
20 | entity_id: alarm_control_panel.house
21 | to: 'armed_home'
22 | action:
23 | - data:
24 | message: 'Alarm Home Mode Armed'
25 | target: email/example@gmail.com
26 | service: notify.pushbullet
27 | - data:
28 | message: 'The house alarm has been switched on in home mode. Goodnight'
29 | service: notify.example_phone_tts
30 |
31 | - id: alarm_arming_away
32 | alias: '[Alarm] Away Mode Arming'
33 | trigger:
34 | - platform: state
35 | entity_id: alarm_control_panel.house
36 | to: 'pending'
37 | action:
38 | - data:
39 | message: 'House alarm activating, ensure all doors and windows are closed'
40 | service: notify.example_phone_tts
41 |
42 | - id: alarm_disarmed
43 | alias: '[Alarm] Disarmed'
44 | trigger:
45 | - platform: state
46 | entity_id: alarm_control_panel.house
47 | to: 'disarmed'
48 | action:
49 | - service: notify.pushbullet
50 | data:
51 | message: 'Alarm Disabled'
52 | target: email/example@gmail.com
53 | - service: switch.turn_off
54 | entity_id: switch.siren_switch
55 | - data:
56 | message: 'The house alarm has been Deactivated'
57 | service: notify.example_phone_tts
58 |
59 | - id: alarm_triggered
60 | alias: '[Alarm] Triggered'
61 | trigger:
62 | - platform: state
63 | entity_id: alarm_control_panel.house
64 | to: 'triggered'
65 | action:
66 | - service: switch.turn_on
67 | entity_id: switch.siren_switch
68 | - service: notify.pushbullet
69 | data:
70 | message: 'ALARM TRIGGERED!!! {{ states[states.alarm_control_panel.house.attributes.changed_by.split(".")[0]][ states.alarm_control_panel.house.attributes.changed_by.split(".")[1]].name }}'
71 | target: email/example@gmail.com
72 |
73 | - id: alarm_warning
74 | alias: '[Alarm] Warning'
75 | trigger:
76 | - platform: state
77 | entity_id: alarm_control_panel.house
78 | to: 'warning'
79 | action:
80 | - service: notify.pushbullet
81 | data:
82 | message: 'ALARM Warning {{ states[states.alarm_control_panel.house.attributes.changed_by.split(".")[0]][ states.alarm_control_panel.house.attributes.changed_by.split(".")[1]].name }}'
83 | target: email/example@gmail.com
84 | - data:
85 | message: 'Hello, the house alarm has been tripped. Please deactivate'
86 | service: notify.example_phone_tts
87 |
88 |
--------------------------------------------------------------------------------
/automation/panic_mode.yaml:
--------------------------------------------------------------------------------
1 | - alias: '[Alarm] Panic Mode'
2 | trigger:
3 | platform: state
4 | entity_id: alarm_control_panel.house
5 | value_template: '{{ state.attributes.panic_mode }}'
6 | to: 'ACTIVE'
7 | action:
8 | service: activate_self_defence_robot
--------------------------------------------------------------------------------
/custom_components/alarm_control_panel/bwalarm.py:
--------------------------------------------------------------------------------
1 | """
2 | CUSTOM ALARM COMPONENT BWALARM
3 | https://github.com/gazoscalvertos/Hass-Custom-Alarm
4 |
5 | VERSION: 1.1.4
6 | MODIFIED: 10/02/19
7 | GazosCalvertos: Yet another take on a custom alarm for Home Assistant
8 |
9 | CHANGE LOG:
10 | -Fixed username issue in log
11 |
12 | """
13 |
14 | REQUIREMENTS = ['ruamel.yaml==0.15.42']
15 |
16 | import asyncio
17 | import sys
18 | import copy
19 | import datetime
20 | import logging
21 | import enum
22 | import os
23 | import re
24 | import json
25 | import pytz
26 | import copy
27 | import hashlib
28 | import time
29 | import uuid
30 |
31 | from homeassistant.const import (
32 | STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
33 | STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME,
34 | CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER,
35 | CONF_DELAY_TIME, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED,
36 | STATE_ON, STATE_OFF)
37 |
38 | from operator import attrgetter
39 | from homeassistant.core import callback
40 | from homeassistant.util.dt import utcnow as now
41 | from homeassistant.loader import bind_hass
42 | from homeassistant.helpers.event import async_track_point_in_time
43 | from homeassistant.helpers.event import async_track_state_change
44 | from homeassistant.util import sanitize_filename
45 |
46 | import voluptuous as vol
47 | import homeassistant.components.alarm_control_panel as alarm
48 | import homeassistant.components.switch as switch
49 | import homeassistant.helpers.config_validation as cv
50 |
51 | _LOGGER = logging.getLogger(__name__)
52 |
53 | VERSION = '1.1.3'
54 |
55 | DOMAIN = 'alarm_control_panel'
56 | #//--------------------SUPPORTED STATES----------------------------
57 | STATE_ALARM_WARNING = 'warning'
58 | STATE_ALARM_ARMED_PERIMETER = 'armed_perimeter'
59 | SUPPORTED_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
60 | STATE_ALARM_TRIGGERED, STATE_ALARM_WARNING, STATE_ALARM_ARMED_PERIMETER]
61 |
62 | SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_PERIMETER]
63 |
64 | #//-------------------STATES TO CHECK------------------------------
65 | STATE_TRUE = 'true'
66 | STATE_UNLOCKED = 'unlocked'
67 | STATE_OPEN = 'open'
68 | STATE_DETECTED = 'detected'
69 | STATE_MOTION = 'motion'
70 | STATE_MOTION_DETECTED = 'motion_detected'
71 | STATE_MOTION_DETECTED2 = 'motion detected'
72 |
73 | STATE_FALSE = 'false'
74 | STATE_LOCKED = 'locked'
75 | STATE_CLOSED = 'closed'
76 | STATE_UNDETECTED = 'undetected'
77 | STATE_NO_MOTION = 'no_motion'
78 | STATE_STANDBY = 'standby'
79 |
80 | CONF_CUSTOM_SUPPORTED_STATUSES_ON = 'custom_supported_statuses_on'
81 | CONF_CUSTOM_SUPPORTED_STATUSES_OFF = 'custom_supported_statuses_off'
82 |
83 | SUPPORTED_STATUSES_ON = [STATE_ON, STATE_TRUE, STATE_UNLOCKED, STATE_OPEN, STATE_DETECTED, STATE_MOTION, STATE_MOTION_DETECTED, STATE_MOTION_DETECTED2]
84 | SUPPORTED_STATUSES_OFF = [STATE_OFF, STATE_FALSE, STATE_LOCKED, STATE_CLOSED, STATE_UNDETECTED, STATE_NO_MOTION, STATE_STANDBY]
85 |
86 | #//-----------------YAML CONFIG OPTIONS----------------------------
87 | CONF_STATES = 'states'
88 | CONF_USERS = 'users'
89 | CONF_NAME = 'name'
90 | CONF_ID = 'id'
91 | CONF_PICTURE = 'picture'
92 | CONF_HOME_PERM = 'home_permision'
93 | CONF_AWAY_PERM = 'away_permission'
94 | CONF_PERI_PERM = 'perimiter_permission'
95 | CONF_ENABLED = 'enabled'
96 | CONF_CODE_TO_ARM = 'code_to_arm'
97 | CONF_PANIC_CODE = 'panic_code'
98 | CONF_PASSCODE_ATTEMPTS = 'passcode_attempts'
99 | CONF_PASSCODE_ATTEMPTS_TIMEOUT = 'passcode_attempts_timeout'
100 | CONF_WARNING_TIME = 'warning_time'
101 |
102 | #//-------------------SENSOR GROUPS--------------------------------
103 | CONF_IMMEDIATE = 'immediate'
104 | CONF_DELAYED = 'delayed'
105 | CONF_IGNORE = 'homemodeignore'
106 | CONF_NOTATHOME = 'notathome'
107 | CONF_OVERRIDE = 'override'
108 | CONF_PERIMETER = 'perimeter'
109 |
110 | #//-----------------DEVICES TO ENABLE/DISBALE-----------------------
111 | CONF_ALARM = 'alarm'
112 | CONF_WARNING = 'warning'
113 |
114 | #//----------------------OPTIONAL MODES------------------------------
115 | CONF_ENABLE_PERIMETER_MODE = 'enable_perimeter_mode'
116 | CONF_ENABLE_PERSISTENCE = 'enable_persistence'
117 |
118 | #//----------------------PANEL RELATED------------------------------
119 | CONF_GUI = 'gui'
120 | CONF_PANEL = 'panel'
121 | CONF_ColorS = 'colors'
122 | CONF_THEMES = 'themes'
123 | CONF_ADMIN_PASSWORD = 'admin_password'
124 | CONF_DISABLE_ANIMATIONS = 'disable_animations'
125 |
126 | #//-----------------------ColorS------------------------------------
127 | CONF_WARNING_Color = 'warning_color'
128 | CONF_PENDING_Color = 'pending_color'
129 | CONF_DISARMED_Color = 'disarmed_color'
130 | CONF_TRIGGERED_Color = 'triggered_color'
131 | CONF_ARMED_AWAY_Color = 'armed_away_color'
132 | CONF_ARMED_HOME_Color = 'armed_home_color'
133 | CONF_PERIMETER_Color = 'perimeter_color'
134 |
135 | #//-----------------------MQTT RELATED-------------------------------
136 | CONF_MQTT = 'mqtt'
137 | CONF_ENABLE_MQTT = 'enable_mqtt'
138 | CONF_OVERRIDE_CODE = 'override_code'
139 | CONF_PAYLOAD_DISARM = 'payload_disarm'
140 | CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
141 | CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
142 | CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
143 | CONF_QOS = 'qos'
144 | CONF_STATE_TOPIC = 'state_topic'
145 | CONF_COMMAND_TOPIC = 'command_topic'
146 | CONF_PENDING_ON_WARNING = 'pending_on_warning'
147 |
148 | #//-----------------------LOG RELATED--------------------------------
149 | CONF_ENABLE_LOG = 'enable_log'
150 | CONF_LOG_SIZE = 'log_size'
151 | CONF_LOGS = 'logs'
152 |
153 | #//-----------------------CAMERA RELATED--------------------------------
154 | CONF_CAMERAS = 'cameras'
155 |
156 | #//-----------------------YAML RELATED--------------------------------
157 | #
158 | CONF_YAML_ALLOW_EDIT = 'yaml_allow_edit'
159 |
160 | class Events(enum.Enum):
161 | ImmediateTrip = 1
162 | DelayedTrip = 2
163 | ArmHome = 3
164 | ArmAway = 4
165 | Timeout = 5
166 | Disarm = 6
167 | Trigger = 7
168 | ArmPerimeter = 8
169 |
170 | class LOG(enum.Enum):
171 | DISARMED = 0 #'disarmed the alarm'
172 | DISARM_FAIL = 1 #'Failed to disarm alarm'
173 | TRIGGERED = 2 #'alarm has been triggered!'
174 | HOME = 3 #'set the alarm in Home mode'
175 | AWAY = 4 #'set the alarm in Away mode'
176 | TRIPPED = 5 #'Alarm has been tripped by: '
177 | LOCKED = 6 #'Panel Locked
178 | PERIMETER = 8 #'set the alarm in Perimeter mode'
179 |
180 | DEFAULT_PENDING_TIME = 0 #0 Seconds
181 | DEFAULT_WARNING_TIME = 0 #0 Seconds
182 | DEFAULT_TRIGGER_TIME = 600 #Ten Minutes
183 |
184 | def _state_validator(config): #Place a default value in that timers if there isnt specific ones set
185 | """Validate the state."""
186 | config = copy.deepcopy(config)
187 | for state in SUPPORTED_PENDING_STATES:
188 | if CONF_TRIGGER_TIME not in config[state]:
189 | config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME]
190 | if CONF_PENDING_TIME not in config[state]:
191 | config[state][CONF_PENDING_TIME] = DEFAULT_STATE_PENDING_TIME if state != STATE_ALARM_ARMED_AWAY else config[CONF_PENDING_TIME]
192 | if CONF_WARNING_TIME not in config[state]:
193 | config[state][CONF_WARNING_TIME] = DEFAULT_STATE_WARNING_TIME if state != STATE_ALARM_ARMED_AWAY else config[CONF_WARNING_TIME]
194 | return config
195 |
196 | def _state_schema():
197 | """Validate the state."""
198 | schema = {}
199 | # if state in SUPPORTED_PENDING_STATES:
200 | schema[vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME)] = vol.All(vol.Coerce(int), vol.Range(min=-1))
201 | schema[vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME)] = vol.All(vol.Coerce(int), vol.Range(min=0))
202 | schema[vol.Optional(CONF_WARNING_TIME, default=DEFAULT_WARNING_TIME)] = vol.All(vol.Coerce(int), vol.Range(min=0))
203 | schema[vol.Optional(CONF_IMMEDIATE, default=[])] = cv.entity_ids # things that cause an immediate alarm
204 | schema[vol.Optional(CONF_DELAYED, default=[])] = cv.entity_ids # things that allow a delay before alarm
205 | schema[vol.Optional(CONF_OVERRIDE, default=[])] = cv.entity_ids # sensors that can be ignored if open when trying to set alarm
206 | return vol.Schema(schema)
207 |
208 | PANEL_SCHEMA = vol.Schema({
209 | vol.Optional(CONF_CAMERAS): cv.entity_ids,
210 | vol.Optional(cv.slug): cv.string,
211 | })
212 |
213 | USER_SCHEMA = vol.Schema([{
214 | vol.Required(CONF_ID, default=uuid.uuid4().hex): cv.string,
215 | vol.Required(CONF_NAME): cv.string,
216 | vol.Optional(CONF_PICTURE, default='/local/images/ha.png'): cv.string,
217 | vol.Required(CONF_CODE): cv.string,
218 | vol.Optional(CONF_ENABLED, default=True): cv.boolean,
219 | vol.Optional(CONF_DISABLE_ANIMATIONS, default=False): cv.boolean
220 | # vol.Optional(CONF_HOME_PERM, default=True): cv.boolean,
221 | # vol.Optional(CONF_AWAY_PERM, default=True): cv.boolean,
222 | # vol.Optional(CONF_PERI_PERM, default=True): cv.boolean
223 | ##ADD TIME BASED SETTINGS
224 | }])
225 |
226 | THEMES_SCHEMA = vol.Schema([{
227 | vol.Optional(cv.slug): cv.string,
228 | }])
229 |
230 | MQTT_SCHEMA = vol.Schema({
231 | vol.Required(CONF_ENABLE_MQTT, default=False): cv.boolean,
232 | vol.Optional(CONF_QOS, default=0): vol.All(vol.Coerce(int), vol.Range(min=0)),
233 | vol.Optional(CONF_STATE_TOPIC, default='home/alarm'): cv.string,
234 | vol.Optional(CONF_COMMAND_TOPIC, default='home/alarm/set'): cv.string,
235 | vol.Optional(CONF_PAYLOAD_ARM_AWAY, default='ARM_AWAY'): cv.string,
236 | vol.Optional(CONF_PAYLOAD_ARM_HOME, default='ARM_HOME'): cv.string,
237 | vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default='ARM_NIGHT'): cv.string,
238 | vol.Optional(CONF_PAYLOAD_DISARM, default='DISARM'): cv.string,
239 | vol.Optional(CONF_OVERRIDE_CODE, default=False): cv.boolean,
240 | vol.Optional(CONF_PENDING_ON_WARNING, default=False): cv.boolean,
241 | })
242 |
243 | PLATFORM_SCHEMA = vol.Schema(vol.All({
244 | vol.Required(CONF_PLATFORM): 'bwalarm',
245 | vol.Optional(CONF_NAME, default='House'): cv.string,
246 | vol.Optional(CONF_PENDING_TIME, default=25): vol.All(vol.Coerce(int), vol.Range(min=0)),
247 | vol.Optional(CONF_WARNING_TIME, default=25): vol.All(vol.Coerce(int), vol.Range(min=0)),
248 | vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(vol.Coerce(int), vol.Range(min=1)),
249 | vol.Optional(CONF_ALARM): cv.entity_id, # switch/group to turn on when alarming [TODO]
250 | vol.Optional(CONF_WARNING): cv.entity_id, # switch/group to turn on when warning [TODO]
251 | vol.Optional(CONF_CUSTOM_SUPPORTED_STATUSES_ON): vol.Schema([cv.string]),
252 | vol.Optional(CONF_CUSTOM_SUPPORTED_STATUSES_OFF): vol.Schema([cv.string]),
253 | vol.Optional(CONF_CODE): cv.string,
254 | vol.Optional(CONF_USERS): USER_SCHEMA, # Schema to hold the list of names with codes allowed to disarm the alarm
255 | vol.Optional(CONF_PANIC_CODE): cv.string,
256 |
257 | #------------------------------STATE RELATED-------------------------
258 | vol.Optional(CONF_STATES): vol.Schema({cv.slug: _state_schema()}),
259 | vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(), #state specific times ###REMOVE###
260 | vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(), #state specific times ###REMOVE###
261 | vol.Optional(STATE_ALARM_ARMED_PERIMETER, default={}): _state_schema(), #state specific times ###REMOVE###
262 | # vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(STATE_ALARM_DISARMED), #state specific times ###REMOVE###
263 | # vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(STATE_ALARM_TRIGGERED), #state specific times ###REMOVE###
264 |
265 | #------------------------------GUI-----------------------------------
266 | vol.Optional(CONF_PANEL): PANEL_SCHEMA,
267 | vol.Optional(CONF_THEMES): THEMES_SCHEMA,
268 |
269 | #---------------------------OPTIONAL MODES---------------------------
270 | vol.Optional(CONF_ENABLE_LOG, default=True): cv.boolean,
271 | vol.Optional(CONF_LOG_SIZE, default=10): vol.All(vol.Coerce(int), vol.Range(min=-1)),
272 | vol.Optional(CONF_LOGS): vol.Schema([cv.string]),
273 | #---------------------------LOG RELATED------------------------------
274 |
275 | vol.Optional(CONF_ENABLE_PERIMETER_MODE, default=False): cv.boolean, # Enable perimeter mode?
276 | vol.Optional(CONF_ENABLE_PERSISTENCE, default=False): cv.boolean, # Enables persistence for alarm state
277 | vol.Optional(CONF_CODE_TO_ARM, default=False): cv.boolean, # Require code to arm alarm?
278 |
279 | #---------------------------PANEL RELATED---------------------------
280 | vol.Optional(CONF_ADMIN_PASSWORD, default='HG28!!&dn'): cv.string, # Admin panel password
281 |
282 | #--------------------------PASSWORD ATTEMPTS--------------------------
283 | vol.Optional(CONF_PASSCODE_ATTEMPTS, default=-1): vol.All(vol.Coerce(int), vol.Range(min=-1)),
284 | vol.Optional(CONF_PASSCODE_ATTEMPTS_TIMEOUT, default=900): vol.All(vol.Coerce(int), vol.Range(min=1)),
285 |
286 | #---------------------------MQTT RELATED------------------------------
287 | vol.Required(CONF_MQTT, default={CONF_ENABLE_MQTT: False}): MQTT_SCHEMA, #vol.Any(MQTT_SCHEMA, None), #cv.boolean, # Allows MQTT functionality
288 |
289 | #---------------------------YAML RELATED----------------------------
290 | vol.Optional(CONF_YAML_ALLOW_EDIT, default=True): cv.boolean, #Allow alarm.yaml to be edited
291 | #-----------------------------END------------------------------------
292 | }, _state_validator))
293 |
294 | SERVICE_YAML_SAVE = 'ALARM_YAML_SAVE'
295 | SERVICE_YAML_USER = 'ALARM_YAML_USER'
296 |
297 | CONF_CONFIGURATION = 'configuration'
298 | CONF_VALUE = 'value'
299 |
300 | CONF_USER = 'user'
301 | CONF_COMMAND = 'command'
302 |
303 | try:
304 | from ruamel.yaml import YAML
305 | except Exception as e:
306 | _LOGGER.warning('Import Error: %s. Attempting to download and import', e)
307 |
308 | @asyncio.coroutine
309 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
310 |
311 | #Setup MQTT if enabled
312 | mqtt = None
313 | if (config[CONF_MQTT][CONF_ENABLE_MQTT]):
314 | import homeassistant.components.mqtt as mqtt
315 |
316 | alarm = BWAlarm(hass, config, mqtt)
317 | hass.bus.async_listen(EVENT_STATE_CHANGED, alarm.state_change_listener)
318 | hass.bus.async_listen(EVENT_TIME_CHANGED, alarm.time_change_listener)
319 | hass.bus.async_listen(EVENT_TIME_CHANGED, alarm.passcode_timeout_listener)
320 | async_add_devices([alarm])
321 |
322 | @callback
323 | def alarm_yaml_save(service):
324 | alarm.settings_save(service.data.get(CONF_CONFIGURATION), service.data.get(CONF_VALUE))
325 |
326 | @callback
327 | def alarm_yaml_user(service):
328 | alarm.settings_user(service.data.get(CONF_USER), service.data.get(CONF_COMMAND))
329 |
330 | hass.services.async_register(DOMAIN, SERVICE_YAML_SAVE, alarm_yaml_save)
331 | hass.services.async_register(DOMAIN, SERVICE_YAML_USER, alarm_yaml_user)
332 |
333 | class BWAlarm(alarm.AlarmControlPanel):
334 |
335 | def __init__(self, hass, config, mqtt):
336 | #------------------------------Initalize the alarm system----------------------------------
337 | self._config = config
338 | self._mqtt = mqtt
339 | self._hass = hass
340 |
341 | self.init_variables()
342 |
343 | self._updateUI = False
344 |
345 | def init_variables(self):
346 | #-------------------------------------STATE SPECIFIC--------------------------------------------------
347 | self._supported_statuses_on = self._config.get(CONF_CUSTOM_SUPPORTED_STATUSES_ON, []) + SUPPORTED_STATUSES_ON
348 | self._supported_statuses_off = self._config.get(CONF_CUSTOM_SUPPORTED_STATUSES_OFF, []) + SUPPORTED_STATUSES_OFF
349 |
350 | self._state = STATE_ALARM_DISARMED
351 | self._returnto = STATE_ALARM_DISARMED
352 | self._armstate = STATE_ALARM_DISARMED
353 |
354 | self._allsensors = []
355 | self._states = {}
356 | for state in self._config.get(CONF_STATES, {}):
357 | self._states[state] = self._config[CONF_STATES][state]
358 | self._allsensors = set(self._allsensors) | set(self._states[state]['immediate']) | set(self._states[state]['delayed']) | set(self._states[state]['override'])
359 |
360 | #-------------------------------------SENSORS--------------------------------------------------
361 | self.immediate = None
362 | self.delayed = None
363 | self.override = None
364 | self._opensensors = None
365 |
366 | #------------------------------------CORE ALARM RELATED-------------------------------------
367 | self._enable_perimeter_mode = self._config[CONF_ENABLE_PERIMETER_MODE]
368 | self._panic_mode = 'deactivated'
369 | self._lasttrigger = ""
370 | self._timeoutat = None
371 | self._passcode_timeoutat = None
372 |
373 | #------------------------------------PASSCODE RELATED-------------------------------------
374 | self._code = self._config.get(CONF_CODE, None)
375 | self._users = self._config.get(CONF_USERS, [])
376 | self._panic_code = self._config.get(CONF_PANIC_CODE, None)
377 | self._panel_locked = False
378 | self._passcodeAttemptNo = 0
379 | self._passcode_attempt_allowed = self._config[CONF_PASSCODE_ATTEMPTS]
380 | self._passcode_attempt_timeout = self._config[CONF_PASSCODE_ATTEMPTS_TIMEOUT]
381 |
382 | #------------------------------------PANEL RELATED-------------------------------------
383 | self.changedbyuser = None
384 |
385 | #-------------------------------------MQTT--------------------------------------------------
386 | # IF MQTT Enabled define its configuration
387 | if (self._config[CONF_MQTT][CONF_ENABLE_MQTT]):
388 | # # If MQTT enabled but is empty then set default values
389 | # if (self._config[CONF_MQTT] == None): self._config[CONF_MQTT] = {}
390 |
391 | self._qos = self._config[CONF_MQTT].get(CONF_QOS)
392 | self._state_topic = self._config[CONF_MQTT].get(CONF_STATE_TOPIC)
393 | self._command_topic = self._config[CONF_MQTT].get(CONF_COMMAND_TOPIC)
394 | self._payload_disarm = self._config[CONF_MQTT].get(CONF_PAYLOAD_DISARM)
395 | self._payload_arm_home = self._config[CONF_MQTT].get(CONF_PAYLOAD_ARM_HOME)
396 | self._payload_arm_away = self._config[CONF_MQTT].get(CONF_PAYLOAD_ARM_AWAY)
397 | self._payload_arm_night = self._config[CONF_MQTT].get(CONF_PAYLOAD_ARM_NIGHT)
398 | self._override_code = self._config[CONF_MQTT].get(CONF_OVERRIDE_CODE)
399 | self._pending_on_warning = self._config[CONF_MQTT].get(CONF_PENDING_ON_WARNING)
400 |
401 | #------------------------------------LOGGING--------------------------------------------------------
402 | # IF logging Enabled define its configuration
403 | if (CONF_ENABLE_LOG in self._config):
404 | self._config[CONF_LOGS] = []
405 | self._log_size = self._config.get(CONF_LOG_SIZE, 10)
406 |
407 | # Get the log file or create one if it doesnt exist
408 | log_path = self._hass.config.path()
409 | if not os.path.isdir(log_path):
410 | _LOGGER.error("[ALARM] Activity Log path %s does not exist.", log_path)
411 | else:
412 | self._log_final_path = os.path.join(log_path, "alarm_log.json")
413 | self.log_load()
414 |
415 |
416 | #------------------------------------YAML--------------------------------------------------------
417 | # self._yaml_allow_edit = self._config[CONF_YAML_ALLOW_EDIT]
418 | # if (self._yaml_allow_edit):
419 | self._yaml_content = self.yaml_load()
420 |
421 | # Reset Alarm
422 | self.clearsignals()
423 |
424 | #------------------------------------PERSISTENCE----------------------------------------------------
425 | self._persistence_list = json.loads('{}')
426 | if (self._config[CONF_ENABLE_PERSISTENCE]):
427 | persistence_path = self._hass.config.path()
428 |
429 | if not os.path.isdir(persistence_path):
430 | _LOGGER.error("[ALARM] Persistence path %s does not exist.", persistence_path)
431 | else:
432 | self._persistence_final_path = os.path.join(persistence_path, "alarm.json")
433 | if (self.persistence_load()):
434 | self._state = self._persistence_list["state"]
435 | self._timeoutat = pytz.UTC.localize(datetime.datetime.strptime(self._persistence_list["timeoutat"].split(".")[0].replace("T"," "), '%Y-%m-%d %H:%M:%S')) if self._persistence_list["timeoutat"] != None else None
436 | self._returnto = self._persistence_list["returnto"]
437 | self._armstate = self._persistence_list["armstate"]
438 |
439 | for self._armstate in SUPPORTED_PENDING_STATES:
440 | self._states = self._persistence_list["states"]
441 | self.immediate = self._states[self._state]["immediate"]
442 | self.delayed = self._states[self._state]["delayed"]
443 | self.override = self._states[self._state]["override"]
444 |
445 | if (self._armstate == STATE_ALARM_WARNING or self._armstate == STATE_ALARM_TRIGGERED or self._armstate == STATE_ALARM_PENDING):
446 | self._states = self._persistence_list["states"]
447 | self.immediate = self._states[self._returnto]["immediate"]
448 | self.delayed = self._states[self._returnto]["delayed"]
449 | self.override = self._states[self._returnto]["override"]
450 |
451 | # Alarm properties
452 | @property
453 | def should_poll(self) -> bool: return False
454 | @property
455 | def name(self) -> str: return self._config[CONF_NAME]
456 | @property
457 | def changed_by(self) -> str: return self._lasttrigger
458 | @property
459 | def state(self) -> str: return self._state
460 | @property
461 | def device_state_attributes(self):
462 |
463 | results = {
464 |
465 | 'immediate': self.immediate,
466 | 'delayed': self.delayed,
467 | 'ignored': self.ignored,
468 | 'allsensors': self._allsensors,
469 |
470 | 'code_to_arm': self._config[CONF_CODE_TO_ARM],
471 |
472 | 'panel_locked': self._panel_locked,
473 | 'passcode_attempts': self._passcode_attempt_allowed,
474 | 'passcode_attempts_timeout': self._passcode_attempt_timeout,
475 |
476 | 'changedbyuser': self.changedbyuser,
477 | 'panic_mode': self._panic_mode,
478 |
479 | 'arm_state': self._armstate,
480 |
481 | 'enable_perimeter_mode': self._config[CONF_ENABLE_PERIMETER_MODE],
482 | 'enable_persistence': self._config[CONF_ENABLE_PERSISTENCE],
483 |
484 | 'enable_log': self._config[CONF_ENABLE_LOG],
485 | 'log_size': self._config[CONF_LOG_SIZE],
486 |
487 | 'supported_statuses_on': self._supported_statuses_on,
488 | 'supported_statuses_off': self._supported_statuses_off,
489 |
490 | 'updateUI': self._updateUI,
491 |
492 | 'admin_password': hashlib.sha256(str.encode(self._config[CONF_ADMIN_PASSWORD])).hexdigest(),
493 |
494 | 'bwalarm_version': VERSION,
495 | 'py_version': sys.version_info,
496 | }
497 |
498 | if (CONF_USERS in self._config):
499 | users = copy.deepcopy(self._config[CONF_USERS])
500 | for user in users:
501 | user['code'] = '****'
502 | results[CONF_USERS] = users
503 |
504 | if (CONF_PANEL in self._config):
505 | results[CONF_PANEL] = self._config[CONF_PANEL]
506 |
507 | if (CONF_THEMES in self._config):
508 | results[CONF_THEMES] = self._config[CONF_THEMES]
509 |
510 | if (CONF_LOGS in self._config):
511 | results[CONF_LOGS] = self._config[CONF_LOGS][-10:]
512 |
513 | if (CONF_MQTT in self._config):
514 | results[CONF_MQTT] = self._config[CONF_MQTT]
515 |
516 | if ('states' in self._config):
517 | results['states'] = self._config['states']
518 |
519 | return results;
520 |
521 | def yaml_load(self):
522 | try:
523 | self.yaml = YAML()
524 | with open(self._hass.config.path() + "/alarm.yaml") as stream:
525 | try:
526 | return self.yaml.load(stream)
527 | except self.yaml.YAMLError as exc:
528 | print(exc)
529 | return None
530 | except Exception as e:
531 | _LOGGER.warning(e);
532 |
533 |
534 | def settings_save(self, configuration=None, value=None):
535 | """Push the alarm state to the given value."""
536 | self._yaml_content = self.yaml_load()
537 |
538 | configuration = configuration.lower()
539 |
540 | self._config[configuration] = value
541 | self._yaml_content[configuration] = value
542 |
543 | _LOGGER.debug("Set the yaml entry %s to %s", configuration, value)
544 |
545 | self.settings_yaml_save()
546 |
547 | def settings_user(self, user=None, command=None):
548 | """Push the alarm state to the given value."""
549 | self._yaml_content = self.yaml_load()
550 |
551 | x = 0
552 |
553 | if (command == 'add'):
554 | if user['id'] == None:
555 | user['id'] = uuid.uuid4().hex
556 | if ('users' not in self._config):
557 | self._config['users'] = [user]
558 | self._yaml_content['users'] = [user]
559 | else:
560 | _LOGGER.warning(user)
561 | self._config['users'].append(user)
562 | self._yaml_content['users'].append(user)
563 | elif (command == 'update'):
564 | for _user in self._config['users']:
565 | if _user['id'] == user['id']:
566 | self._config['users'][x] = user
567 | self._yaml_content['users'][x] = user
568 | x = x + 1
569 | elif (command == 'delete'):
570 | for _user in self._config['users']:
571 | if _user['id'] == user:
572 | self._config['users'].pop(x)
573 | self._yaml_content['users'].pop(x)
574 | x = x + 1
575 | elif (command == True or command == False):
576 | for _user in self._config['users']:
577 | if _user['id'] == user:
578 | self._config['users'][x]['enabled'] = command
579 | self._yaml_content['users'][x]['enabled'] = command
580 | x = x + 1
581 |
582 | self.settings_yaml_save()
583 |
584 | def settings_yaml_save(self):
585 | #Trigger a GUI update
586 | self._updateUI = not self._updateUI
587 |
588 | with open(self._hass.config.path() + "/alarm.yaml", 'w') as fil:
589 | self.yaml.dump(self._yaml_content, fil)
590 |
591 | self.init_variables()
592 | self.schedule_update_ha_state()
593 |
594 |
595 | ### LOAD persistence previously saved
596 | def persistence_load(self):
597 | try:
598 | if os.path.isfile(self._persistence_final_path): #Find the persistence JSON file and load. Once found update the alarm_control_panel object
599 | self._persistence_list = json.load(open(self._persistence_final_path, 'r'))
600 | return True
601 | else: #No persistence file found
602 | _LOGGER.warning("[ALARM] Persistence file doesnt exist")
603 | return False
604 | #self._persistence_list = json.loads('{"state":"disarmed", "timeoutat":null, "returnto":null, "immediate":[], "delayed":[], "override":[], "states":{}, "armstate":"disarmed"}')
605 |
606 | except Exception as e:
607 | _LOGGER.error("[ALARM] Persistence error occured loading: %s", str(e))
608 |
609 | ### UPDATE persistence
610 | def persistence_save(self, persistence):
611 | if persistence is not None: #Check we have something to save [TODO] validate this is a persistence object
612 | self._persistence_list = persistence
613 | try:
614 | if self._persistence_list is not None: #Check we have genuine persistence to save if so dump to file
615 | with open(self._persistence_final_path, 'w') as fil:
616 | fil.write(json.dumps(self._persistence_list, ensure_ascii=False))
617 | else:
618 | _LOGGER.error("[ALARM] No persistence to save!")
619 | except Exception as e:
620 | _LOGGER.error("[ALARM] Persistence Error occured saving: %s", str(e))
621 |
622 | ### LOAD activity log previously saved
623 | def log_load(self):
624 | try:
625 | if os.path.isfile(self._log_final_path): #Find the log file and load.
626 | self._config[CONF_LOGS] = json.load(open(self._log_final_path, 'r'))
627 | else: #No log file found
628 | _LOGGER.warning("[ALARM] Activity log file doesnt exist")
629 | self._config[CONF_LOGS] = []
630 | self.log_save()
631 | except Exception as e:
632 | _LOGGER.error("[ALARM] Error occured loading: %s", str(e))
633 |
634 | ### UPDATE activity log
635 | def log_save(self):
636 | try:
637 | if self._config[CONF_LOGS] is not []: #Check we have genuine log to save if so dump to file
638 | with open(self._log_final_path, 'w') as fil:
639 | fil.write(json.dumps(self._config[CONF_LOGS], ensure_ascii=False))
640 | else:
641 | _LOGGER.error("[ALARM] No log to save!")
642 | except Exception as e:
643 | _LOGGER.error("[ALARM] Log Error occured saving: %s", str(e))
644 |
645 |
646 | ### Save alarm state
647 | def save_alarm_state(self):
648 | self._persistence_list["state"] = self._state
649 | self._persistence_list["timeoutat"] = self._timeoutat.isoformat() if self._timeoutat != None else None
650 | self._persistence_list["returnto"] = self._returnto
651 | self._persistence_list["states"] = self._states
652 | self._persistence_list["armstate"] = self._armstate
653 | self.persistence_save(self._persistence_list)
654 |
655 | ### Actions from the outside world that affect us, turn into enum events for internal processing
656 | def time_change_listener(self, eventignored):
657 | """ I just treat the time events as a periodic check, its simpler then (re-/un-)registration """
658 | if self._timeoutat is not None:
659 | if now() > self._timeoutat:
660 | self._timeoutat = None
661 | self.process_event(Events.Timeout)
662 |
663 | def state_change_listener(self, event):
664 | """ Something changed, we only care about things turning on at this point """
665 | if self._state != STATE_ALARM_DISARMED:
666 | new = event.data.get('new_state', None)
667 | if new is None:
668 | return
669 |
670 | if new.state != None:
671 | if new.state.lower() in self._supported_statuses_on:
672 | eid = event.data['entity_id']
673 | if eid in self.immediate:
674 | self._lasttrigger = eid
675 | self.process_event(Events.ImmediateTrip)
676 | elif eid in self.delayed:
677 | self._lasttrigger = eid
678 | self.process_event(Events.DelayedTrip)
679 |
680 | # def check_open_sensors(self):
681 | # for sensor in self._allsensors:
682 | # if self._hass.states.get(sensor).state != None:
683 | # if self._hass.states.get(sensor).state in self._supported_statuses_on:
684 | # _LOGGER.error(self._hass.states.get(sensor)) # do summit
685 |
686 | @property
687 | def code_format(self):
688 | """One or more characters."""
689 | return None if self._code is None else '.+'
690 |
691 | def alarm_disarm(self, code=None):
692 | #If the provided code matches the panic alarm then deactivate the alarm but set the state of the panic mode to active.
693 | if self._validate_panic_code(code):
694 | self.process_event(Events.Disarm)
695 | self._panic_mode = "ACTIVE"
696 | self._update_log(None, LOG.DISARMED, None) #Show a default disarm message incase this is displayed on the interface
697 | # Let HA know that something changed
698 | self.schedule_update_ha_state()
699 | return
700 |
701 | if not self._validate_code(code):
702 | self._update_log(None, LOG.DISARM_FAIL, None)
703 | return
704 | self.process_event(Events.Disarm)
705 |
706 | def alarm_arm(self, code, mode):
707 | user = 'HA'
708 | if code == "override": #ARM THE ALARM IMMEDIATELY
709 | self.process_event(mode, True)
710 | else:
711 | for entity in self._users:
712 | if entity['enabled'] == True:
713 | if entity['code'] == code:
714 | user = entity['id']
715 | self.process_event(mode)
716 |
717 | if self._config[CONF_CODE_TO_ARM]:
718 | if code == self._code:
719 | self.process_event(mode)
720 | else:
721 | self.process_event(mode)
722 |
723 | self._update_log(user, mode, None)
724 |
725 | def alarm_arm_home(self, code=None):
726 | self.alarm_arm(code, Events.ArmHome)
727 |
728 | def alarm_arm_away(self, code=None):
729 | self.alarm_arm(code, Events.ArmAway)
730 |
731 | def alarm_arm_night(self, code=None):
732 | self.alarm_arm(code, Events.ArmPerimeter)
733 |
734 | def alarm_trigger(self, code=None):
735 | self.process_event(Events.Trigger)
736 | self._update_log(None, LOG.TRIGGERED, None)
737 |
738 | ### Internal processing
739 | def setsignals(self, alarmMode):
740 | """ Figure out what to sense and how """
741 | self.immediate = self._states[alarmMode]['immediate'].copy()
742 | self.delayed = self._states[alarmMode]['delayed'].copy()
743 | self.override = self._states[alarmMode]['override'].copy()
744 | self.ignored = set(self._allsensors) - (set(self.immediate) | set(self.delayed))
745 |
746 | def clearsignals(self):
747 | """ Clear all our signals, we aren't listening anymore """
748 | self._panic_mode = "deactivated"
749 | self._armstate = STATE_ALARM_DISARMED
750 | self.immediate = set()
751 | self.delayed = set()
752 | self.ignored = self._allsensors.copy()
753 | self._timeoutat = None
754 |
755 | def process_event(self, event, override_pending_time=False):
756 | old_state = self._state
757 |
758 | #Update the state of the alarm panel
759 | if event == Events.Disarm:
760 | self._state = STATE_ALARM_DISARMED
761 |
762 | elif event == Events.Trigger:
763 | self._state = STATE_ALARM_TRIGGERED
764 |
765 | #If there is a pending time set in either of the state configs then drop into pending mode else simply arm the alarm
766 | elif old_state == STATE_ALARM_DISARMED:
767 | if event == Events.ArmHome:
768 | if (datetime.timedelta(seconds=int(self._states[STATE_ALARM_ARMED_HOME][CONF_PENDING_TIME])) and override_pending_time == False):
769 | self._state = STATE_ALARM_PENDING
770 | else:
771 | self._state = STATE_ALARM_ARMED_HOME
772 | self._armstate = STATE_ALARM_ARMED_HOME
773 |
774 | elif event == Events.ArmAway:
775 | if (datetime.timedelta(seconds=int(self._states[STATE_ALARM_ARMED_AWAY][CONF_PENDING_TIME])) and override_pending_time == False):
776 | self._armstate = STATE_ALARM_ARMED_AWAY
777 | self._state = STATE_ALARM_PENDING
778 | else:
779 | self._state = STATE_ALARM_ARMED_AWAY
780 | self._armstate = STATE_ALARM_ARMED_AWAY
781 |
782 | elif event == Events.ArmPerimeter:
783 | if (datetime.timedelta(seconds=int(self._states[STATE_ALARM_ARMED_PERIMETER][CONF_PENDING_TIME])) and override_pending_time == False):
784 | self._armstate = STATE_ALARM_ARMED_PERIMETER
785 | self._state = STATE_ALARM_PENDING
786 | else:
787 | self._state = STATE_ALARM_ARMED_PERIMETER
788 | self._armstate = STATE_ALARM_ARMED_PERIMETER
789 |
790 | elif old_state == STATE_ALARM_PENDING:
791 | if event == Events.Timeout: self._state = self._armstate
792 |
793 | elif old_state == STATE_ALARM_ARMED_HOME or \
794 | old_state == STATE_ALARM_ARMED_AWAY or \
795 | old_state == STATE_ALARM_ARMED_PERIMETER:
796 | if event == Events.ImmediateTrip: self._state = STATE_ALARM_TRIGGERED
797 | elif event == Events.DelayedTrip: self._state = STATE_ALARM_WARNING
798 |
799 | elif old_state == STATE_ALARM_WARNING:
800 | if event == Events.Timeout: self._state = STATE_ALARM_TRIGGERED
801 |
802 | elif old_state == STATE_ALARM_TRIGGERED:
803 | if event == Events.Timeout: self._state = self._returnto
804 |
805 | new_state = self._state
806 | if old_state != new_state:
807 | _LOGGER.debug("[ALARM] Alarm changing from {} to {}".format(old_state, new_state))
808 | # Things to do on entering state
809 | if new_state == STATE_ALARM_WARNING:
810 | _LOGGER.debug("[ALARM] Turning on warning")
811 | if self._config.get(CONF_WARNING):
812 | self._hass.services.call(self._config.get(CONF_WARNING).split('.')[0], 'turn_on', {'entity_id':self._config.get(CONF_WARNING)})
813 | self._timeoutat = now() + datetime.timedelta(seconds=int(self._states[self._armstate][CONF_WARNING_TIME]))
814 | self._update_log(None, LOG.TRIPPED, self._lasttrigger)
815 | elif new_state == STATE_ALARM_TRIGGERED:
816 | _LOGGER.debug("[ALARM] Turning on alarm")
817 | if self._config.get(CONF_ALARM):
818 | self._hass.services.call(self._config.get(CONF_ALARM).split('.')[0], 'turn_on', {'entity_id':self._config.get(CONF_ALARM)})
819 | if (self._states[self._armstate][CONF_TRIGGER_TIME] == -1):
820 | self._timeoutat = now() + datetime.timedelta(hours=int(24))
821 | else:
822 | self._timeoutat = now() + datetime.timedelta(seconds=int(self._states[self._armstate][CONF_TRIGGER_TIME]))
823 | self._update_log(None, LOG.TRIPPED, self._lasttrigger)
824 | elif new_state == STATE_ALARM_PENDING:
825 | _LOGGER.debug("[ALARM] Pending user leaving house")
826 | if self._config.get(CONF_WARNING):
827 | self._hass.services.call(self._config.get(CONF_WARNING).split('.')[0], 'turn_on', {'entity_id':self._config.get(CONF_WARNING)})
828 | self._timeoutat = now() + datetime.timedelta(seconds=int(self._states[self._armstate][CONF_PENDING_TIME]))
829 | #self._returnto = STATE_ALARM_ARMED_AWAY
830 | self.setsignals(self._armstate)
831 | elif new_state == STATE_ALARM_ARMED_HOME:
832 | self._returnto = new_state
833 | self.setsignals(STATE_ALARM_ARMED_HOME)
834 | elif new_state == STATE_ALARM_ARMED_AWAY:
835 | self._returnto = new_state
836 | self.setsignals(STATE_ALARM_ARMED_AWAY)
837 | elif new_state == STATE_ALARM_ARMED_PERIMETER:
838 | self._returnto = new_state
839 | self.setsignals(STATE_ALARM_ARMED_PERIMETER)
840 | elif new_state == STATE_ALARM_DISARMED:
841 | self._returnto = new_state
842 | self.clearsignals()
843 |
844 | # Things to do on leaving state
845 | if old_state == STATE_ALARM_WARNING or old_state == STATE_ALARM_PENDING:
846 | _LOGGER.debug("[ALARM] Turning off warning")
847 | if self._config.get(CONF_WARNING):
848 | self._hass.services.call(self._config.get(CONF_WARNING).split('.')[0], 'turn_off', {'entity_id':self._config.get(CONF_WARNING)})
849 |
850 | elif old_state == STATE_ALARM_TRIGGERED:
851 | _LOGGER.debug("[ALARM] Turning off alarm")
852 | if self._config.get(CONF_ALARM):
853 | self._hass.services.call(self._config.get(CONF_ALARM).split('.')[0], 'turn_off', {'entity_id':self._config.get(CONF_ALARM)})
854 |
855 | # Let HA know that something changed
856 | if self._config[CONF_ENABLE_PERSISTENCE]:
857 | self.save_alarm_state()
858 | self.schedule_update_ha_state()
859 |
860 | def _validate_code(self, code):
861 | """Validate given code."""
862 | if ((int(self._passcode_attempt_allowed) == -1) or (self._passcodeAttemptNo <= int(self._passcode_attempt_allowed))):
863 | check = self._code is None or code == self._code or self._validate_user_codes(code)
864 | if code == self._code:
865 | self._update_log(None, LOG.DISARMED, None)
866 | return self._validate_code_attempts(check)
867 | else:
868 | _LOGGER.warning("[ALARM] Too many passcode attempts, try again later")
869 | return False
870 |
871 | def _validate_user_codes(self, code):
872 | for entity in self._users:
873 | if entity['enabled'] == True:
874 | if entity['code'] == code:
875 | self._update_log(entity['id'], LOG.DISARMED, None)
876 | return True
877 | return False
878 |
879 | def _validate_code_attempts(self, check):
880 | if check:
881 | self._passcodeAttemptNo = 0
882 | else:
883 | _LOGGER.debug("[ALARM] Invalid code given")
884 | self._passcodeAttemptNo += 1
885 | if (int(self._passcode_attempt_allowed) != -1 and self._passcodeAttemptNo > int(self._passcode_attempt_allowed)):
886 | self._panel_locked = True
887 | self._passcode_timeoutat = now() + datetime.timedelta(seconds=int(self._passcode_attempt_timeout))
888 | _LOGGER.warning("[ALARM] Panel locked, too many passcode attempts!")
889 | self._update_log(None, LOG.LOCKED, None)
890 | self.schedule_update_ha_state()
891 | return check
892 |
893 | def _validate_panic_code(self, code):
894 | """Validate given code."""
895 | check = code == self._panic_code
896 | if check:
897 | _LOGGER.warning("[ALARM] PANIC MODE ACTIVATED!!!")
898 | self._passcodeAttemptNo = 0
899 | return check
900 |
901 | def _update_log(self, id, message, entity_id):
902 | if (id == None or id == ''):
903 | id = 'HA'
904 | self.changedbyuser = id
905 | if (CONF_ENABLE_LOG in self._config):
906 | self._log_size = int(self._config[CONF_LOG_SIZE]) if CONF_LOG_SIZE in self._config else 10
907 | if self._log_size != -1 and len(self._config[CONF_LOGS]) >= self._log_size:
908 | self._config[CONF_LOGS].remove(self._config[CONF_LOGS][0])
909 | self._config[CONF_LOGS].append([time.time(), id, message.value, entity_id])
910 | self.log_save()
911 |
912 | ### Actions from the outside world that affect us, turn into enum events for internal processing
913 | def passcode_timeout_listener(self, eventignored):
914 | if self._passcode_timeoutat is not None:
915 | if now() > self._passcode_timeoutat:
916 | self._panel_locked = False
917 | self._passcode_timeoutat = None
918 | self._passcodeAttemptNo = 0
919 | self.schedule_update_ha_state()
920 |
921 | @asyncio.coroutine
922 | def async_added_to_hass(self):
923 | """Subscribe mqtt events.
924 | This method must be run in the event loop and returns a coroutine.
925 | """
926 | if (self._config[CONF_MQTT][CONF_ENABLE_MQTT]):
927 | async_track_state_change(
928 | self._hass, self.entity_id, self._async_state_changed_listener
929 | )
930 |
931 | @callback
932 | def message_received(topic, payload, qos):
933 | """Run when new MQTT message has been received."""
934 | #_LOGGER.warning("[ALARM] MQTT Topic: %s Payload: %s", topic, payload)
935 | if payload.split(" ")[0] == self._payload_disarm:
936 | #_LOGGER.warning("Disarming %s", payload)
937 | #TODO self._hass.states.get('binary_sensor.siren_sensor') #Use this method to relay open states
938 | if (self._override_code):
939 | self.alarm_disarm(self._code)
940 | else:
941 | self.alarm_disarm(payload.split(" ")[1])
942 | elif payload == self._payload_arm_home:
943 | self.alarm_arm_home('')
944 | elif payload == self._payload_arm_away:
945 | self.alarm_arm_away('')
946 | elif payload == self._payload_arm_night:
947 | self.alarm_arm_night('')
948 | else:
949 | _LOGGER.warning("[ALARM/MQTT] Received unexpected payload: %s", payload)
950 | return
951 | if (self._config[CONF_MQTT][CONF_ENABLE_MQTT]):
952 | return self._mqtt.async_subscribe(
953 | self._hass, self._command_topic, message_received, self._qos)
954 |
955 | @asyncio.coroutine
956 | def _async_state_changed_listener(self, entity_id, old_state, new_state):
957 | """Publish state change to MQTT."""
958 | if (self._config[CONF_MQTT][CONF_ENABLE_MQTT]):
959 | state = new_state.state
960 | if (self._pending_on_warning == True and state == STATE_ALARM_WARNING):
961 | state = STATE_ALARM_PENDING
962 |
963 | self._mqtt.async_publish(self._hass, self._state_topic, state, self._qos, True)
964 |
965 | _LOGGER.debug("[ALARM/MQTT] State changed")
966 |
--------------------------------------------------------------------------------
/example automations/panic_mode.yaml:
--------------------------------------------------------------------------------
1 | - alias: '[Alarm] Panic Mode'
2 | trigger:
3 | platform: state
4 | entity_id: alarm_control_panel.house
5 | value_template: '{{ state.attributes.panic_mode }}'
6 | to: 'ACTIVE'
7 | action:
8 | service: activate_self_defence_robot
--------------------------------------------------------------------------------
/guidance/configuration.md:
--------------------------------------------------------------------------------
1 | ## Installation Guide
2 | To get this running add the files (alarm.yaml, panels/alarm.html, custom_components/alarm_control_panel/bwalarm.py, www/lib/countdown360.js, www/lib/jquery-3.2.1.min.js, www/alarm/alarm.css) from this repo into your home assistant configuration directory, then add the following to your configuration.yaml file:
3 |
4 | **NOTE:** If you already have a panel_custom.yaml for say floorplan then just copy and paste the code from this repo file into your own panel_custom.yaml to prevent floorplan from being overritten.
5 | **NOTE:** Same goes for Automations.yaml. Append the samples inside of this file into your own automations.yaml
6 |
7 | ```
8 | alarm_control_panel: !include alarm.yaml
9 | panel_custom: !include panel_custom.yaml
10 | ```
11 |
12 | **NOTE:** If you experience issues with the page not displaying then add the following:
13 | ```
14 | #CONFIGURATION.YAML
15 | frontend:
16 | javascript_version: latest
17 | ```
18 | ## Configuration variables:
19 |
20 | **Alarm.yaml configuration settings:**
21 |
22 | - platform: bwalarm **#[REQUIRED, String] Name of the custom alarm component. Do not change**
23 | - name: House **#[REQUIRED, String]This can be changed to whatever suits your need, ensure this attribute matches the one in your panel_custom.yaml 'alarmid: alarm_control_panel.house'**
24 |
25 | - code: '9876' **#[REQUIRED, digits] should consist of one or more digits ie '6482' ensure your passcode is encapsulated by quotes**
26 | - panic_code: '1234' **#[OPTIONAL, digits] Panic Code should consist of one or more digits ie '1234' ensure your passcode is encapsulated by quotes, it needs to be different to your standard alarm code. This enables a special panic mode. This can be used under duress to deactivate the alarm which would appear to the unseeing eye as deactivated however a special attribute [panic_mode] listed under the alarm_control_panel.[identifier] will change to ACTIVE. This status could be used in your automations to send a notification to someone else police/spouse/sibling/neighbour that you are under duress. To deactive this mode arm then disarm your alarm in the usual manner.**
27 |
28 | - alarm: automation.alarm_triggered **#[REQUIRED, String] The automation to fire when the alarm has been triggered**
29 | - warning: automation.alarm_warning **#[OPTIONAL, String] The automation to fire when the alarm has been tripped**
30 | #### [OPTIONAL SETTINGS]
31 | - clock: True **#[OPTIONAL, Boolean] False by default. True enables a clock in the center of the status bar**
32 | - perimeter_mode: True **#[OPTIONAL, Boolean] False by default. True enables perimeter mode, this could be known as 'Day Mode' i.e. only arm the doors whilst there is someone using all floors**
33 | - weather: True **#[OPTIONAL, Boolean] False by Default. Allows a weather summary to be displayed on the status bar. Dark Sky weather component must be enabled with the name sensor.dark_sky_summary**
34 | - persistence: False **#[OPTIONAL, Boolean] False by Default. Allows this custom component to save the state of the alarm to file then reinstate it in the event of power loss.**
35 | - hide_passcode: True **#[OPTIONAL, Boolean] True by default. This is a security feature when enabled hides the passcode while entering disarm code.**
36 | - hide_sidebar: True **#[OPTIONAL, Boolean] False by default. This is a security feature when enabled hides the HA sidebar when the alarm is armed. The sidebar re-appears when the alarm is disarmed.**
37 | - hide_sensor_groups: True **#[OPTIONAL, Boolean] - False by default. Setting this to True hides sensor groups (all sensors, immediate sensors, delayed sensors, inactive sensors) from the display. Open sensors will still appear**
38 | -hide_custom_panel: True **#[OPTIONAL, Boolean] - True by default. Setting this to False enables a custom panel below the sensors groups which allows you to add your own html code. Use this to bring any other features you would like to see for example displaying live camera feeds, a rotating image gallery, custom HA buttons and sensors. To use this enable the custom panel in alarm.yaml (custom_panel: True) then ensure you take a copy of custom-element.html and add it to you www/alarm/ folder. Edit the html code between the template tags. I'm have added a custom sample folder where I will upload examples of 'things' which can be added here. Please contribute!!!**
39 |
40 | ## Timings
41 | - **pending_time:** 25 #[OPTIONAL, Number, default 25] Grace time in seconds to allow for exit and entry using Away mode.
42 | - **trigger_time:** 600 #[OPTIONAL, Number, default 600] The time in seconds of the trigger time in which the alarm is firing. before returning previous set alarm state.
43 |
44 | ### [STATES]
45 | - **armed_perimeter:** #[OPTIONAL]
46 | **pending_time:** 10 #[OPTIONAL] State specific setting if not defined inherits from above top level time
47 | **trigger_time:** 300 #[OPTIONAL] State specific setting if not defined inherits from above top level time
48 | #[OPTIONAL however either an immediate or delayed group must exist] Sensors in this group tigger the alarm immediately
49 | immediate:
50 | - binary_sensor.your_sensors
51 | #[OPTIONAL] Sensors in this group start the clock (pending_time) when tripped before the alarm is triggered
52 | delayed:
53 | - binary_sensor.your_sensors
54 | #[OPTIONAL] Use this group to automatically override the warning message on open sensors when arming. (I use this as I have a motion sensor at the front door)
55 | override:
56 | - binary_sensor.your_sensor
57 |
58 | - **armed_home:** #[REQUIRED]
59 | **pending_time:** 10 #[OPTIONAL] State specific setting if not defined inherits from above top level time
60 | **trigger_time:** 300 #[OPTIONAL] State specific setting if not defined inherits from above top level time
61 | #[OPTIONAL however either an immediate or delayed group must exist] Sensors in this group tigger the alarm immediately
62 | immediate:
63 | - binary_sensor.your_sensors
64 | #[OPTIONAL] Sensors in this group start the clock (pending_time) when tripped before the alarm is triggered
65 | delayed:
66 | - binary_sensor.your_sensors
67 | #[OPTIONAL] Use this group to automatically override the warning message on open sensors when arming. (I use this as I have a motion sensor at the front door)
68 | override:
69 | - binary_sensor.your_sensor
70 |
71 | - **armed_away:** #[REQUIRED]
72 | **pending_time:** 25 #[OPTIONAL] State specific setting if not defined inherits from above top level time
73 | **trigger_time:** 600 #[OPTIONAL] State specific setting if not defined inherits from above top level time
74 | #[OPTIONAL however either an immediate or delayed group must exist] Sensors in this group tigger the alarm immediately
75 | immediate:
76 | - binary_sensor.your_sensors
77 | #[OPTIONAL] Sensors in this group start the clock (pending_time) when tripped before the alarm is triggered
78 | delayed:
79 | - binary_sensor.your_sensors
80 | #[OPTIONAL] Use this group to automatically override the warning message on open sensors when arming. (I use this as I have a motion sensor at the front door)
81 | override:
82 | - binary_sensor.your_sensor
83 |
84 | ### [PASSCODE RELATED]
85 | - passcode_attempts: 3 #[OPTIONAL, number] Disabled if commented out. When a value equal or greater than 0 is set, the system will only allow the set amount of password attempts before timing out
86 | - passcode_attempts_timeout: 30 #[OPTIONAL, number] Default 30 seconds. When set with the password attempts option the panel will timeout for the amount of seconds set if the password is entered incorrectly as per the password_attempts option. The system will then reset the allowed password attempts
87 |
88 | ### [MQTT RELATED]
89 | - mqtt: True #[OPTIONAL, boolean] False by default. Settings this to True will enable MQTT Mode. Uncomment options below to use See the README for guidance.
90 | - override_code: True #[OPTIONAL, boolean] False by default. if true allows MQTT commands to disarm the alarm without a valid code.
91 | - state_topic: 'home/alarm' #[OPTIONAL, string] The MQTT topic HA will publish state updates to.
92 | - command_topic: 'home/alarm/set' #[OPTIONAL, string] The MQTT topic HA will subscribe to, to receive commands from a remote device to change the alarm state.
93 | - qos: 0 #[OPTIONAL, number] The maximum QoS level for subscribing and publishing to MQTT messages. Default is 0.
94 | - payload_disarm: "DISARM" #[OPTIONAL, string] The payload to disarm this Alarm Panel. Default is “DISARM”.
95 | - payload_arm_home: "ARM_HOME" #[OPTIONAL, string] The payload to set armed-home mode on this Alarm Panel. Default is “ARM_HOME”.
96 | - payload_arm_away: "ARM_AWAY" #[OPTIONAL, string] The payload to set armed-away mode on this Alarm Panel. Default is “ARM_AWAY”.
97 | - payload_arm_night: "ARM_NIGHT" #[OPTIONAL, string] The payload to set armed-night mode on this Alarm Panel. Default is “ARM_NIGHT”.
98 |
99 | ### [COLOURS] Use any HTML format
100 | - warning_colour: 'orange' #[OPTIONAL, string]
101 | - pending_colour: 'orange' #[OPTIONAL, string]
102 | - disarmed_colour: '#03A9F4' #[OPTIONAL, string]
103 | - armed_home_colour: 'black' #[OPTIONAL, string]
104 | - armed_away_colour: 'black' #[OPTIONAL, string]
105 | - triggered_colour: 'red' #[OPTIONAL, string]
106 |
107 | ### [CUSTOM STATUSES]
108 | -custom_supported_statuses_on: #[OPTIONAL, list of strings] CUSTOM SENSOR STATUSES - These settings allow devices which are not natively supported by this panel to be used. This is to be used when the state of the device is not recognised by the panel. Examples are provided below
109 | - 'running' #EXAMPLE
110 | -custom_supported_statuses_off:
111 | - 'not_running' #EXAMPLE
--------------------------------------------------------------------------------
/historic_changelog.md:
--------------------------------------------------------------------------------
1 | - (07/03/18) NEW Feature - Pending and Trigger time can be determined per state. For example arming the alarm in away mode can be set to 60 seconds pending time, yet arming in home mode can be set to 30 seconds. This can also be overriden as per the feature below which has been expanded to include home/perimiter. [See guidance here](guidance/configuration.md#timings)
2 |
3 | - (23/02/18) NEW FEATURE - Override the pending time when arming away mode so that the alarm arms instantly. To do this pass a code parameter of '-1' to the service, this could be in the form of an automation such as:
4 | ```
5 | alias: '[Alarm] Instantly Arm Away Mode'
6 | trigger:
7 | ... place your trigger here ...
8 | action:
9 | - service: alarm_control_panel.alarm_arm_away
10 | entity_id: alarm_control_panel.house
11 | data:
12 | code: '-1'
13 | ```
14 |
15 | - (16/02/18) NEW FEATURE - PERSISTENCE!!!!!!!! Enabling persistence in the alarm.yaml file allows this component to save the alarm state everytime it changes. This means if there is a power outage or server crash/failure then upon restart HA will auto load the previous alarm state. This has not been tested in windows but in theory should work. Ensure HA has permission to write to the config folder as this creates a new file named alarm.json.
16 | - (16/02/18) NEW FEATURE - Custom Panel allowing custom html/polymer code!!!!!! Use this to bring any other features you would like to see for example displaying live camera feeds, a rotating image gallery, custom HA buttons and sensors. To use this enable the custom panel in alarm.yaml (custom_panel: True) then ensure you take a copy of custom-element.html and add it to you www/alarm/ folder. Edit the html code between the template tags. I'm have added a custom sample folder where I will upload examples of 'things' which can be added here. Please contribute!!!
17 | - (16/02/18) NEW FEATURE - Added the ability to hide the sensor groups (all sensors, immediate sensors, delayed sensors, inactive sensors) from the display. Open sensors will still appear on the display. (hide_sensor_groups:True) see alarm.yaml for details.
18 | - (16/02/18) NEW FEATURE - Added the ability to mask the passcode during diasarm. The feature will be enabled by deault. See the alarm.yaml for extra information. Credit @mikefero
19 |
20 | - (16/02/18) BUG FIX - Added further code to force the sidebar to hide when the alarm is armed. This prevents a simple refresh showing the sidebar.
21 | - (16/02/18) BUG FIX - Given the time label has caused folk issues I have decided to drop the javascript implementation and use the time derived from HA. Ensure you have a time sensor setup in your sensors.yaml:
22 | ```
23 | - platform: time_date
24 | display_options:
25 | - 'time'
26 | ```
27 | - (15/02/18) BUG FIX - Fixed the code disarm issue.
28 |
29 | - (19/01/18) NEW FEATURE - MQTT now allows you to disarm your alarm using the your code. MQTT panels will need to support the format of the payload which is 'DISARM CODE' for example 'DISARM 0000'. To override this so that MQTT can disarm the alarm without passing across the code then set override_code: True in the alarm.yaml. Status feedback to MQTT coming soon...
30 | - (19/01/18) NEW FEATURE - The panel now allows you to hide the sidebar when the alarm is activated preventing in intruder to simply go to configuration and shut down HA. A suitable locked down browser will also be required to prevent the intruder simply changing the URL. You could check out kiosk on android. To activate this feature simply enable hide_sidebar: True in the alarm.yaml NOTE!! Ensure you copy alarm_scripts.js into the appropriate folder 'www/alarm'. This was a tricky feature to implement and future HA updates may break this. If anyone has a better idea on how to code this then be my guest.
31 |
32 | - (19/01/18) ADDITIONAL STATE - Added 'motion_detected' as a supported state
33 |
34 | - (15/01/18) NOTE!!!!!!! - There are a lot of changes, update all files to ensure everything works. (alarm.yaml, panels/alarm.html, custom_components/alarm_control_panel/bwalarm.py, www/lib/countdown360.js, www/lib/jquery-3.2.1.min.js, www/alarm/alarm.css) Also don't forget to clear your browser cache. Raise any issues you come across
35 |
36 | - (15/01/18) NEW FEATURE - Set a maximum number of Passcode attempts with a lockout period. See the alarm.yaml for setup.
37 | - (15/01/18) NEW FEATURE - There is now support for custom and unknown device states. Add your new on/off states into the alarm.yaml file and this component will track them!
38 |
39 | - (15/01/18) NEW GUI - The code panel slides in or out depending on the mode. Let me know what you think.
40 |
41 | - (15/01/18) BUG FIX - Weather alignment on different modes and devices.
42 | - (15/01/18) BUG FIX - Timer mode now appears in warning mode so you know how long before the alarm triggers.
43 | - (15/01/18) BUG FIX - MQTT now allows custom state/command topics via the alarm.yaml.
44 | - (15/01/18) BUG FIX - Re-aligned buttons for smaller devices.
45 |
46 | - (15/01/18) OTHER - Work progressing to clean up the python code.
47 | - (15/01/18) OTHER - CSS is now split from the main http file for readability, still requires a cleanse.
48 |
49 | - (15/01/18) COMING SOON - MQTT Support to valid passcodes.
50 | - (15/01/18) COMING SOON - MQTT Support to provide extended feedback to other alarm interfaces i.e. open sensor arrays, lockout feedback.
51 | - (15/01/18) COMING SOON - Persistance support for rebooted systems.
52 | - (15/01/18) COMING SOON - Disabling of the sidebar when armed. Trickier than I thought due to the use of polymer and its sandboxing.
53 | - (15/01/18) COMING SOON - User Guide/Help pages
54 | - (15/01/18) COMING SOON - Customizable settings such as gui colours, collapsable sensor groups
55 | - (15/01/18) COMING SOON - Screensaver mode
56 | - (15/01/18) COMING SOON - Granular times on home/arm mode
57 | - (15/01/18) COMING SOON - Screenshots + Videos
58 |
59 | - (08/01/17) NEW FEATURE - MQTT Integration. Enable this by setting mqtt to True in the yaml. See alarm.yaml for optional settings. This is based on the [manual mqtt code](https://home-assistant.io/components/alarm_control_panel.manual_mqtt/). [MQTT Needs to be enabled in your HA setup and configured appropriately](https://home-assistant.io/docs/mqtt/broker/#embedded-broker) then you should be able to use [custom panels such as this](https://play.google.com/store/apps/details?id=com.thanksmister.iot.mqtt.alarmpanel&hl=en)
60 |
61 | - (28/12/17) Added a new feature 'Panic Mode' this allows you to set a panic code in the alarm.yaml. When using this code to deactivate the alarm, the alarm is deactivated however a special attribute panic_mode is set to ACTIVE. Use this backed with your automations to trigger custom messages to those who can assist.
62 | - (28/12/17) Added support for override sensors. When sensors are placed in this group any which are open when activing the alarm are ignored.
63 |
64 | - (27/12/17) Added support for devices with open/closed, true/false, locked/unlocked, detected/undetected statuses. There are some heavy changes on the code in readiness for a settings page and an optional screensaver.
65 |
66 | - (19/11/17) Added optional perimeter mode (activates a 'perimeter' group only) which could also ne known as 'Home Day' mode. Added weather sensor into status bar (You must have dark sky weather component enabled specifically sensor.dark_sky_summary), added 0 to code panel.
67 |
68 | - (13/11/17) Added sample automation.yaml. Fixed GUI issues with groups. Outlined base code for 'Perimeter mode'
69 |
70 | - (12/11/17) You can now use either homemodeignore or notathome group title for sensors that need to be ignored during home mode
71 | - (12/11/17) Added a check (displays open sensors in highlighted group) for open sensors when setting alarm (changes button text to override alarm). **NOTE** override in alarm.yaml isn't quite ready yet and you will still need to manually override via the button in the interface for now.
--------------------------------------------------------------------------------
/panel_custom.yaml:
--------------------------------------------------------------------------------
1 | - name: alarm
2 | sidebar_title: Alarm
3 | sidebar_icon: mdi:security-home
4 | config:
5 | alarmid: alarm_control_panel.house
6 |
--------------------------------------------------------------------------------
/www/alarm/alarm.css:
--------------------------------------------------------------------------------
1 | /* CUSTOM ALARM COMPONENT PANEL CSS
2 | https://github.com/gazoscalvertos/Hass-Custom-Alarm
3 | VERSION: 1.1.4
4 | MODIFIED: 13/11/18
5 | */
6 | :host {
7 | --countdown-timer-display: none;
8 | --time-display: initial;
9 |
10 | /* THEME - DARK */
11 | --dark-background: #191A1F;
12 | --dark-inner: #212227;
13 |
14 | --dark-green: #23C48E;
15 | --dark-black: #1A1E22;
16 |
17 | --dark-grey: #7F7F7F;
18 | --dark-grey-text: #BCBCBE;
19 |
20 | --dark-white-text: #FFFFFF;
21 | --dark-greyout-text: #77787A;
22 |
23 | --dark-red: #D3435C;
24 | --dark-light-blue: #04C0F8;
25 | --dark-orange: #ED9D00;
26 |
27 | --dark-indigo: #5771C2;
28 | --dark-indigo-grey: #37446B;
29 |
30 | /* THEME - LIGHT */
31 | --light-background: white;
32 | --light-inner: #f3f3f3;
33 |
34 | --light-green: #23C48E;
35 | --light-black: #1A1E22;
36 |
37 | --light-grey: #7F7F7F;
38 | --light-grey-text: #BCBCBE;
39 |
40 | --light-white-text: #FFFFFF;
41 | --light-greyout-text: #77787A;
42 |
43 | --light-red: #D3435C;
44 | --light-light-blue: #04C0F8;
45 | --light-orange: #ED9D00;
46 |
47 | --light-indigo: #5771C2;
48 | --light-indigo-grey: #37446B;
49 |
50 | --button-shape: 0%;
51 |
52 | /* GUI SPECIFIC */
53 | --primary-text-color: 'white';
54 |
55 | --panel-background-color: var(--dark-background); /*The background color of the main content section.*/
56 |
57 | /* --panel-outer-background-color: var(--secondary-background-color); */
58 | --panel-outer-background-color: var(--dark-inner); /*The background color of both the status bar and the menu bar*/
59 | --panel-text-color: var(--dark-white-text); /*The color of the general text within the panel.*/
60 |
61 | --header-background-color: var(--dark-background); /*The background color of very top header bar.*/
62 | --header-text-color: var(--dark-white-text); /*The text color on very top header bar.*/
63 |
64 | --alarmstatus-text-color: var(--dark-green); /*The text color of the Alarm Status*/
65 | --time-text-color: var(--dark-white-text); /*The text color of the Time label*/
66 | --weather-text-color: var(--dark-white-text); /*The text color of the Weather label.*/
67 | --weather-image-color: var(--dark-light-blue); /*The color of the Weather image*/
68 |
69 | --info-header-text-color: white; /* The color of the Heading within a particular Section*/
70 | --info-detail-text-color: var(--dark-green); /*The color of the descriptive text within a particular Section.*/
71 |
72 | --title-color: var(--dark-orange); /*The color of the Title text within a particular section.*/
73 | --subtitle-text-color: var(--dark-light-blue); /*The color of the Subtitle text within a particular section.*/
74 | --openSensors-title-color: var(--dark-orange); /*The color of the Open Sensors Title to draw attention to the open sensor*/
75 | --button-background-color: var(--dark-background); /*The background color of the alarm buttons.*/
76 | --cancel-color: var(--dark-red); /*The background color of the cancel button.*/
77 | --override-color: var(--dark-orange); /* The background color of the override button.*/
78 | --action-button-border-color: var(--dark-green);/* The border color of the action button.*/
79 |
80 | --info-panel-buttons-color: var(--dark-light-blue); /*The color of the menu buttons*/
81 |
82 | --arm-button-border-color: var(--dark-light-blue);/* The border color of the alarm buttons.*/
83 | --arm-button-text-color: white;/* The color of the text within the alarm buttons.*/
84 |
85 | --paper-listbox-background-color: blue;/* The background color of the listboxes.*/
86 | --paper-listbox-color: var(--dark-light-blue);/* The text color within the listboxes.*/
87 |
88 | --paper-item-selected_-_color: var(--dark-orange);/* The text color of the item selected within a selection box.*/
89 | --paper-input-container-shared-input-style_-_color: white;
90 |
91 | --font-header: unset; /*Font of the header, time and weather text*/
92 |
93 | --shadow-effect: below 0 linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, .1));
94 |
95 | }
96 | app-toolbar {
97 | background-color: var(--header-background-color);
98 | color: var(--header-text-color);
99 | }
100 | #layout {
101 | background-color: var(--panel-outer-background-color);
102 | display: flex;
103 | flex-direction: column;
104 | height: 100%;
105 | min-height: 100%;
106 | width: 100%;
107 | overflow: hidden;
108 | }
109 | #main-title {
110 | text-align: center !important;
111 | position: relative !important;
112 | display: block !important;
113 | background-color: var(--header-background-color);
114 | padding: 10px;
115 | }
116 | ha-menu-button + [main-title] {
117 | margin-left: -50px;
118 | }
119 | ha-menu-button {
120 | color: var(--header-text-color);
121 | float: left;
122 | }
123 | a, a:link, a:visited, a:active {
124 | color: var(--panel-text-color);
125 | text-decoration: none;
126 | }
127 |
128 | a:hover {
129 | text-decoration: underline;
130 | color: var(--panel-text-color);
131 | }
132 |
133 | .list-item {
134 | cursor: pointer;
135 | font-size: large;
136 | }
137 | .checkbox {
138 | cursor: pointer;
139 | padding: 5px;
140 | font-size: large;
141 | }
142 |
143 | .detail, .show, .hide:target {
144 | display: none;
145 | }
146 | .hide:target + .show,
147 | .hide:target ~ .detail {
148 | display: block;
149 | }
150 |
151 | #modalError {
152 | position: absolute;
153 | z-index: 1;
154 | margin-top: 25vh;
155 | }
156 |
157 | #error-icon {
158 | width: 60%;
159 | text-align: center;
160 | margin: auto;
161 | }
162 | #error-icon iron-icon{
163 | height: 100px;
164 | width: 100px;
165 | color: var(--title-color);
166 | }
167 | #error-title {
168 | font-size: 40pt;
169 | line-height: 40pt;
170 | color: var(--subtitle-text-color);
171 | margin-bottom: 10px;
172 | }
173 | #error-header {
174 | font-size: x-large;
175 | line-height: 20pt;
176 | color: var(--title-color);
177 | margin-bottom: 10px;
178 | }
179 | #error-detail {
180 | font-size: large;
181 | line-height: 15pt;
182 | color: var(--info-detail-text-color);
183 | margin-bottom: 10px;
184 | }
185 |
186 |
187 | .iron-selected:before{
188 | content: '✔'; /* Insert content that looks like bullets */
189 | padding-right: 8px;
190 | color: var(--dark-orange); /* Or a color you prefer */
191 | }
192 |
193 | #restart{
194 | background-color: unset;
195 | padding-left: 0;
196 | margin-left: 0;
197 | text-transform: none;
198 | }
199 |
200 | #userDetail, #themeDetail{
201 | border: var(--primary-text-color);
202 | border-width: 0.5px;
203 | border-style: groove;
204 | padding-left: 20px;
205 | padding-right: 20px;
206 |
207 | }
208 | #settings-alarm, #settings-all, #settings-info, #settings-sensors, #settings-cameras, #settings-floorplan, #settings-mqtt {
209 | /* padding-bottom: 100px; */
210 | }
211 | .iron-selected {
212 | color: var(--title-color);
213 | }
214 |
215 | .validation-error{
216 | border: red;
217 | border-style: groove;
218 | border-width: 2px;
219 | }
220 |
221 | .action-button{
222 | border: 3px solid var(--action-button-border-color) !important;
223 | }
224 |
225 | .dropdown-content{
226 | width: 90%;
227 | }
228 |
229 | .dropdown-content iron-icon {
230 | padding-right: 10px;
231 | }
232 | .sensors {
233 | padding-right: 20px;
234 | }
235 |
236 | .checkbox{
237 | padding-left:10px;
238 | padding-right:10px;
239 | }
240 |
241 | .tappable {
242 | cursor: pointer;
243 | margin-top: 2px;
244 | font-size: 1.2em;
245 | }
246 |
247 | #container ::slotted(.content) {
248 | padding: 15px;
249 | }
250 | #warning-button {
251 | color: var(--dark-orange) !important;
252 | }
253 | .shake{
254 | animation: shake linear 1s;
255 | animation-iteration-count: 1;
256 | transform-origin: 50% 50%;
257 | -webkit-animation: shake linear 1s;
258 | -webkit-animation-iteration-count: 1;
259 | -webkit-transform-origin: 50% 50%;
260 | -moz-animation: shake linear 1s;
261 | -moz-animation-iteration-count: 1;
262 | -moz-transform-origin: 50% 50%;
263 | -o-animation: shake linear 1s;
264 | -o-animation-iteration-count: 1;
265 | -o-transform-origin: 50% 50%;
266 | -ms-animation: shake linear 1s;
267 | -ms-animation-iteration-count: 1;
268 | -ms-transform-origin: 50% 50%;
269 | }
270 | #controls{
271 | padding-top: 5px;
272 | }
273 | #carousel_main{
274 | position: relative;
275 | /* height: 60vh; */
276 | padding: 15px;
277 |
278 | /* transition: all 1s ease-in-out; */
279 | /* -webkit-transition: all 1s ease-in-out; /** Chrome & Safari **/
280 | /* -moz-transition: all 1s ease-in-out; /** Firefox **/
281 | /* -o-transition: all 1s ease-in-out; /** Opera **/
282 | }
283 | #carousel_settings{
284 | position: relative;
285 | z-index: 1;
286 | }
287 |
288 | .carousel-slide-in {
289 | /* transform: translate(-1600px,0); */
290 | /* -webkit-transform: translate(-1600px,0); /** Chrome & Safari **/
291 | /* -o-transform: translate(-1600px,0); /** Opera **/
292 | /* -moz-transform: translate(-1600px,0); /** Firefox **/
293 |
294 | }
295 |
296 | .margin-left {
297 | margin-left: -15px;
298 | }
299 |
300 | .carousel-cell {
301 | /* left: 1600px; */
302 | top: 0;
303 | /* position: absolute; */
304 |
305 | /* transition: all 1s ease-in-out; */
306 | /* -webkit-transition: all 1s ease-in-out; /** Chrome & Safari **/
307 | /* -moz-transition: all 1s ease-in-out; /** Firefox **/
308 | /* -o-transition: all 1s ease-in-out; /** Opera **/
309 |
310 | width: 100%;
311 | /* margin-bottom: 60px; */
312 | color: var(--panel-text-color);
313 | }
314 |
315 | ha-label-badge {
316 | --ha-label-badge-color: rgb(223, 76, 30);
317 | }
318 | :host ::slotted(.badge-container.ha-label-badge) {
319 | margin: 0px 12px;
320 | }
321 | .actions {
322 | margin-left: 20px;
323 | @apply(--layout-self-center);
324 | }
325 | .flickity-viewport {
326 | /* transition: height 0.5s; */
327 | }
328 | .box {
329 | display: -webkit-box;
330 | display: -ms-flexbox;
331 | display: flex;
332 | -webkit-box-pack: center;
333 | -ms-flex-pack: center;
334 | justify-content: center; /* align horizontal */
335 | -webkit-box-align: center;
336 | -ms-flex-align: center;
337 | align-items: center; /* align vertical */
338 | }
339 |
340 | .box-sensors {
341 | width: 100%;
342 | }
343 | .title{
344 | font-size: x-large;
345 | }
346 | .sensor-title{
347 | font-size: x-large;
348 | }
349 | .title, .sensor-title{
350 | color: var(--subtitle-text-color);
351 | text-align: center;
352 | font-weight: 500;
353 | padding-top: 15px;
354 | }
355 | .openSensors {
356 | margin-left: 8%;
357 | margin-right: 8%;
358 | color: var(--openSensors-title-color);
359 | font-size: x-large;
360 | text-align: center;
361 | font-weight: 500;
362 | padding: 15px 0px;
363 | }
364 |
365 | .summary {
366 | margin: 0px auto 0px;
367 | text-align: center;
368 | }
369 | .cameras {
370 | cursor: pointer;
371 | }
372 |
373 | .camera-caption {
374 | @apply --paper-font-common-nowrap;
375 | position: relative;
376 | bottom: 46px;
377 | /* border-bottom-left-radius: 2px;
378 | border-bottom-right-radius: 2px;*/
379 | background-color: rgba(0, 0, 0, 0.3);
380 | padding: 10px;
381 | font-size: 16px;
382 | font-weight: 500;
383 | color: white;
384 | }
385 |
386 | .content-box {
387 | display: flex;
388 | flex-direction: column;
389 | justify-content: center;
390 | }
391 | /* ################################################# height > 600px ############################################# */
392 | @media screen and (min-height: 600px) {
393 | #status-bar {
394 | margin-top: 15px;
395 | margin-bottom: 15px;
396 | height: 87px;
397 | }
398 | }
399 |
400 | /* ################################################# width XS < 767 ############################################# */
401 | @media screen and (max-width: 767px) {
402 |
403 | #carousel_settings {
404 | margin-right: 10px;
405 | margin-left: 10px;
406 | /*text-align: center;*/
407 | }
408 | #carousel_settings h1{
409 | text-align: center;
410 | }
411 | #carousel_settings li{
412 | text-align: left;
413 | }
414 | .log-item-date {
415 | font-weight: 300;
416 | font-style: italic;
417 | font-size: smaller;
418 | }
419 | .camera-feed {
420 | width: 300px;
421 | }
422 | .content-box {
423 | width: 100%;
424 | }
425 | #code-display {
426 | margin-top: 10px;
427 | }
428 | #main-content {
429 | /* padding-bottom: 30px; */
430 | }
431 | div.arm paper-button {
432 | width: 36vh;
433 | height: 36vh;
434 | margin: 15px;
435 | font-size: large;
436 | }
437 | #alarm-status span {
438 | font-size: x-large;
439 | }
440 | .sm {
441 | display: none !important;
442 | }
443 | .sign {
444 | font-size: x-large;
445 | }
446 | #clock, #weather {
447 | display: none;
448 | }
449 | #hour, #minute, #middle {
450 | font-size: xx-large;
451 | }
452 | #meridiem {
453 | font-size: 10px;
454 | margin-left: -20px;
455 | margin-right: 3px;
456 | }
457 | #weather-icon img {
458 | width: 40px;
459 | }
460 | .weather {
461 | -webkit-box-pack: center;
462 | -ms-flex-pack: center;
463 | justify-content: center;
464 | -webkit-box-orient: vertical;
465 | -webkit-box-direction: normal;
466 | -ms-flex-direction: column;
467 | flex-direction: column;
468 | display: -webkit-box;
469 | display: -ms-flexbox;
470 | display: flex;
471 | color: var(--weather-text-color);
472 | }
473 | .vLayout {
474 | -webkit-box-orient: vertical;
475 | -webkit-box-direction: normal;
476 | display: -webkit-box;
477 | display: -ms-flexbox;
478 | display: flex;
479 | flex-wrap: wrap;
480 | justify-content: center;
481 | text-align: center;
482 | }
483 | div.arm paper-button {
484 | /* margin-bottom: 10px;*/
485 | }
486 | div.digits paper-button, div.digits paper-icon-button{
487 | color: var(--arm-button-text-color);
488 | border-radius: var(--button-shape);
489 | min-width: 11vh;
490 | height: 11vh;
491 | padding: 0;
492 | border: 2px solid var(--arm-button-border-color);
493 | background-color: var(--button-background-color);
494 | margin: 8px;
495 | font-weight: 400;
496 | display: flex;
497 | }
498 | #code-display {
499 | margin-bottom: 15px;
500 | }
501 | }
502 | /* ############## SM > 768 ############### */
503 |
504 | #info-panel-selection paper-icon-button {
505 | width: 12vw;
506 | height: 12vw;
507 | }
508 |
509 | @media screen and (min-width: 768px) {
510 | .content-box {
511 | /*height: 100%;*/
512 | width: 100%;
513 | padding-bottom: 20px;
514 | }
515 | .cameras{
516 | padding-right: 15px;
517 | }
518 | .camera-feed {
519 | width: 400px;
520 |
521 | }
522 |
523 | .log-item{
524 | display:flex;
525 | flex-direction:row;
526 | width: 100%;
527 | }
528 |
529 | .log-item > span{
530 | flex-grow:1;
531 | }
532 |
533 | .log-item > div{
534 | flex-grow:0;
535 | font-size: large;
536 | margin: auto;
537 | font-family: var(--paper-font-body1_-_font-family);;
538 | }
539 |
540 | .log-item-date {
541 | font-weight: 300;
542 | font-style: italic;
543 | text-align: right;
544 | }
545 | #main-content {
546 | /* padding-top: 20px;
547 | padding-bottom: 120px; */
548 | }
549 | #code-display {
550 | margin-top: 20px;
551 | margin-bottom: 40px;
552 | }
553 | div.arm paper-button {
554 | width: 36vh;
555 | height: 36vh;
556 | /* line-height: 110px;*/
557 | margin: 20px;
558 | padding: 20px;
559 | font-size: x-large;
560 | }
561 | #alarm-status span {
562 | font-size: xx-large;
563 | }
564 | .xs {
565 | display: none !important;
566 | }
567 | .sign {
568 | font-size: xx-large;
569 | }
570 | #meridiem {
571 | font-size: small;
572 | margin-left: -29px;
573 | }
574 | #weather, #clock {
575 | width: 30%;
576 | }
577 | #alarm-status {
578 | width: 40%
579 | }
580 | #weather-icon img {
581 | width: 70px;
582 | }
583 | .vLayout {
584 | -webkit-box-orient: horizontal;
585 | -webkit-box-direction: normal;
586 | display: -webkit-box;
587 | display: -ms-flexbox;
588 | justify-content: center;
589 | display: flex;
590 | min-width: 50%;
591 | justify-content: center;
592 | flex-wrap: wrap;
593 | text-align: center;
594 | }
595 | div.digits paper-button, div.digits paper-iron-button{
596 | border-radius: var(--button-shape);
597 | width: 18vh;
598 | height: 18vh;
599 | border: 2px solid var(--arm-button-border-color);
600 | background-color: var(--button-background-color);
601 | display: flex;
602 | vertical-align: middle;
603 | margin: 10px;
604 | padding: 20px;
605 | text-align: center;
606 | font-size: xx-large;
607 | }
608 | }
609 |
610 | /* ######################################################### width => 1024 ################################################# */
611 | @media screen and (min-width: 1024px) {
612 | #weather, #clock, #alarm-status {
613 | width: 33%;
614 | }
615 | .info-detail {
616 | font-size: large;
617 | }
618 | h1 {
619 | font-size: x-large;
620 | }
621 |
622 | .settings-nav-inner{
623 | margin-right: 20px;
624 | }
625 |
626 | .settings-nav-inner paper-icon-button {
627 | width: 60px;
628 | height: 60px;
629 |
630 | }
631 |
632 |
633 | #info-panel-selection paper-icon-button {
634 | width: 75px;
635 | height: 75px;
636 | margin-left: 15px;
637 | }
638 | span, li, .settings-nav-text{
639 | font-size: large;
640 | }
641 |
642 |
643 | }
644 |
645 | /* ######################################################### width => 1200 ################################################# */
646 | @media screen and (min-width: 1200px) {
647 | .md {
648 | display: none !important;
649 | }
650 | .lg{
651 | display: flex !important;
652 | }
653 | .initial {
654 | display: flex;
655 | }
656 |
657 | .content-box {
658 | width: 75%;
659 | }
660 | #controls {
661 | /* width: 50% !important;
662 | max-height: 60vh;
663 | height: auto;*/
664 | }
665 |
666 |
667 |
668 | }
669 |
670 |
671 | /* ######################################################### ALL DEVICES ################################################# */
672 | #icon {
673 | margin-right: 15px;
674 | }
675 | paper-listbox {
676 | color: var(--info-panel-buttons-color);
677 | background: unset;
678 | }
679 | #content {
680 | display: flex;
681 | flex-direction: column;
682 | flex: 1 1 auto;
683 | width: 100%;
684 | overflow-y: auto;
685 | overflow-x: hidden;
686 | }
687 | h1 {
688 | color: var(--title-color);
689 | }
690 |
691 | #settings-nav {
692 | color: var(--primary-text-color);
693 | justify-content: center;
694 | text-align: center;
695 | padding-top: 10px;
696 | }
697 | #set-login {
698 | text-align: center;
699 | }
700 | .settings-nav-inner {
701 | /* max-width: 40px;*/
702 | padding: 3px;
703 | }
704 | /*.settings-nav-text {
705 | font-size: x-small;
706 | }*/
707 |
708 | .info-header {
709 | color: var(--info-header-text-color);
710 | font-size: large;
711 | padding-right: 5px;
712 | }
713 | .setting-outer {
714 | padding-top: 10px;
715 | padding-bottom: 10px;
716 | }
717 | .setting-inner {
718 | padding-bottom: 5px;
719 | width: 100%;
720 | padding-right: 10px;
721 | }
722 | .setting-input > paper-input{
723 | flex-grow: 3;
724 | }
725 | .setting-input > paper-icon-button{
726 | flex-grow: 1;
727 | width: 50px;
728 | height: 50px;
729 | }
730 | .setting-toggle {
731 | align-items: center;
732 | }
733 | #info-panel-selection {
734 | color: var(--info-panel-buttons-color);
735 | background: var(--panel-outer-background-color);
736 | position: relative;
737 | display: inline;
738 | flex-wrap: wrap;
739 | align-items: center;
740 | text-align: center;
741 | justify-content: space-between;
742 | padding: .5rem 1rem;
743 | }
744 |
745 | #main-content {
746 | display: flex;
747 | justify-content: center;
748 | flex-direction: row;
749 | background-color: var(--panel-background-color);
750 | }
751 |
752 | .sign {
753 | margin-left: -20px;
754 | }
755 | .ok {
756 | color: green;
757 | }
758 | .warning {
759 | color: orange;
760 | }
761 | .danger {
762 | color: red;
763 | }
764 |
765 | .name {
766 | text-align: center;
767 | }
768 | div.countdown-timer {
769 | display: var(--countdown-timer-display);
770 | }
771 |
772 | div.code {
773 | text-transform: capitalize;
774 | padding-top: 23px;
775 | }
776 | #code-display {
777 | text-align: center;
778 | font-size: xx-large;
779 | letter-spacing: 5px;
780 | color: var(--panel-text-color);
781 | padding-bottom: 10px;
782 | width: 150px;
783 | border-bottom: solid;
784 | border-bottom-width: 1.3px;
785 | margin-top: auto;
786 | margin-right: auto;
787 | margin-left: auto;
788 | height: 20px;
789 | }
790 |
791 | div.code iron-label {
792 | font-size: x-large;
793 | }
794 |
795 | paper-button {
796 | background: #6b8d9c;
797 | color: #ffffff;
798 | }
799 |
800 |
801 | div.arm paper-button {
802 | background-color: var(--button-background-color);
803 | border-radius: var(--button-shape);
804 | border: 3px solid var(--arm-button-border-color);
805 | display: flex;
806 | color: var(--arm-button-text-color);
807 | }
808 | div.arm paper-button div {
809 | border-radius: var(--button-shape);
810 | width: 100px;
811 | height: 100px;
812 | border: 3px solid var(--arm-button-border-color);
813 | display: flex;
814 | line-height: 100px;
815 | margin: 20px;
816 | padding: 20px;
817 | }
818 | div.arm paper-button.big {
819 | width: 68%;
820 | }
821 | div.arm paper-button.cancel {
822 | border: 3px solid var(--cancel-color);
823 | }
824 | div.arm paper-button.override {
825 | border: 3px solid var(--override-color);
826 | }
827 |
828 | div.options {
829 | padding-top: 10px;
830 | }
831 | div.options paper-button{
832 | width: 96.5%;
833 | }
834 | .tappable {
835 | cursor: pointer;
836 | margin-top: 2px;
837 | font-size: 1.2em;
838 | }
839 | .shadow {
840 | padding-bottom: 6px;
841 | -webkit-box-reflect: var(--shadow-effect);
842 | }
843 | #time, #time span {
844 | color: var( --time-text-color);
845 | text-shadow: 1px 1px #6b8d9b;
846 | display: var(--time-display);
847 | font-size: xx-large;
848 | font-family: var(--font-header);
849 | }
850 |
851 | #middle {
852 | text-align: center;
853 | padding-left: 8px;
854 |
855 | }
856 | #meridiem {
857 | display: inline-block;
858 | vertical-align: middle;
859 | line-height: normal;
860 | height: 100%;
861 | color: #6b8d9b;
862 | }
863 | #alarm-status {
864 | flex-direction: column;
865 | text-transform: capitalize;
866 | }
867 | #alarm-status span {
868 | color: var( --alarmstatus-text-color);
869 | font-weight: bolder;
870 | text-align: center;
871 | }
872 | #weather-icon {
873 | width: 100px;
874 | text-align: center;
875 | fill: var( --weather-image-color);
876 | }
877 | #weather-icon object {
878 | transform: scale(3,3);
879 | }
880 | .weather-summary span {
881 | display: inline-block;
882 | vertical-align: middle;
883 | font-size: x-large;
884 | margin-left: 5px;
885 | padding-right: 5px;
886 | text-align: center;
887 | height: 100%;
888 | font-family: var(--font-header);
889 | color: var( --weather-text-color);
890 | }
891 |
892 | #main-title-text{
893 | flex-wrap: wrap;
894 | text-align: center;
895 |
896 | font-size: xx-large;
897 | font-family: var(--font-header);
898 | justify-content: space-between;
899 |
900 | padding: .5rem 1rem;
901 | color: var(--header-text-color)
902 | }
903 | .hide-bar {
904 | margin-top: -64px;
905 | height: calc(100vh + 64px);
906 | }
907 |
908 | .view {
909 | position: relative;
910 | top: 24vh;
911 | left: 0;
912 | right: 0;
913 | bottom: 0;
914 | -webkit-perspective: 400;
915 | perspective: 400;
916 | }
917 | #codepanel {
918 | overflow: hidden;
919 | }
920 |
921 | @-webkit-keyframes shake{
922 | 0% {
923 | -webkit-transform: translate(0px,0px) ;
924 | }
925 | 10% {
926 | -webkit-transform: translate(-10px,0px) ;
927 | }
928 | 20% {
929 | -webkit-transform: translate(10px,0px) ;
930 | }
931 | 30% {
932 | -webkit-transform: translate(-10px,0px) ;
933 | }
934 | 40% {
935 | -webkit-transform: translate(10px,0px) ;
936 | }
937 | 50% {
938 | -webkit-transform: translate(-10px,0px) ;
939 | }
940 | 60% {
941 | -webkit-transform: translate(10px,0px) ;
942 | }
943 | 70% {
944 | -webkit-transform: translate(-10px,0px) ;
945 | }
946 | 80% {
947 | -webkit-transform: translate(10px,0px) ;
948 | }
949 | 90% {
950 | -webkit-transform: translate(-10px,0px) ;
951 | }
952 | 100% {
953 | -webkit-transform: translate(0px,0px) ;
954 | }
955 | }
956 |
957 | @keyframes shake{
958 | 0% {
959 | transform: translate(0px,0px) ;
960 | }
961 | 10% {
962 | transform: translate(-10px,0px) ;
963 | }
964 | 20% {
965 | transform: translate(10px,0px) ;
966 | }
967 | 30% {
968 | transform: translate(-10px,0px) ;
969 | }
970 | 40% {
971 | transform: translate(10px,0px) ;
972 | }
973 | 50% {
974 | transform: translate(-10px,0px) ;
975 | }
976 | 60% {
977 | transform: translate(10px,0px) ;
978 | }
979 | 70% {
980 | transform: translate(-10px,0px) ;
981 | }
982 | 80% {
983 | transform: translate(10px,0px) ;
984 | }
985 | 90% {
986 | transform: translate(-10px,0px) ;
987 | }
988 | 100% {
989 | transform: translate(0px,0px) ;
990 | }
991 | }
992 |
993 | .remove {
994 | display: none !important;
995 | }
996 |
997 | .info-detail {
998 | color: var(--info-detail-text-color);
999 | padding-top: 2px;
1000 | }
1001 |
1002 | /* The Modal (background) */
1003 | #modalCamera, #modalDonate {
1004 | display: none; /* Hidden by default */
1005 | align-items: center;
1006 | position: absolute; /* Stay in place */
1007 | z-index: 1; /* Sit on top */
1008 | padding-top: 5vh; /* Location of the box */
1009 | left: 0;
1010 | top: 0;
1011 | width: 100%; /* Full width */
1012 | height: calc(100% - 5vh); /* Full height */
1013 | overflow: hidden; /* Enable scroll if needed */
1014 | background-color: rgb(0,0,0); /* Fallback color */
1015 | background-color: rgba(0,0,0,0.9); /* Black w/ opacity */
1016 | }
1017 |
1018 |
1019 | /* Modal Content (Image) */
1020 | #imgCamera {
1021 | margin: auto;
1022 | display: block;
1023 | width: 80%;
1024 | max-width: 100vh;
1025 | }
1026 |
1027 | /* Caption of Modal Image (Image Text) - Same Width as the Image */
1028 | #captionCamera {
1029 | margin: auto;
1030 | display: block;
1031 | width: 100%;
1032 | text-align: center;
1033 | padding: 10px 0;
1034 | height: 150px;
1035 |
1036 | @apply --paper-font-common-nowrap;
1037 | position: relative;
1038 | bottom: 46px;
1039 | border-bottom-left-radius: 2px;
1040 | border-bottom-right-radius: 2px;
1041 | background-color: rgba(0, 0, 0, 0.3);
1042 | font-size: 16px;
1043 | font-weight: 500;
1044 | color: white;
1045 | }
1046 |
1047 | #donateMethod{
1048 | font-size: xx-large;
1049 | padding-bottom: 20px;
1050 | }
1051 |
1052 | #donateAddress, #donateMethod {
1053 | margin: auto;
1054 | display: block;
1055 | width: 100%;
1056 | text-align: center;
1057 |
1058 | padding-top: 20px;
1059 |
1060 | font-weight: 500;
1061 | }
1062 |
1063 | #donateAddress {
1064 | font-size: large;
1065 | overflow-wrap: break-word;
1066 | }
1067 |
1068 | /* Add Animation - Zoom in the Modal */
1069 | .modal-camera-content, #captionCamera {
1070 | animation-name: zoomCamera;
1071 | animation-duration: 1.6s;
1072 | }
1073 |
1074 | .qr {
1075 | background-repeat: no-repeat;
1076 | background-size: contain;
1077 | margin: auto;
1078 | display: block;
1079 |
1080 | width: 256px;
1081 | height: 256px;
1082 | }
1083 |
1084 | .logo {
1085 | background-repeat: no-repeat;
1086 | background-size: contain;
1087 | width: 48px;
1088 | height: 48px;
1089 | margin: 10px auto 10px auto;
1090 | }
1091 |
1092 | .donate, .donate-paypal {
1093 | text-align: center;
1094 | padding-top: 10px;
1095 | padding-right: 10px;
1096 | max-width: 200px;
1097 | overflow-wrap: break-word;
1098 |
1099 | }
1100 |
1101 | .logo{
1102 | -webkit-filter: invert(1);
1103 | filter: invert(1);
1104 | cursor: pointer;
1105 | }
1106 |
1107 | .flexwrap {
1108 | flex-wrap: wrap;
1109 | }
1110 |
1111 | .donate-box {
1112 | max-width: 200px;
1113 | overflow-wrap: break-word;
1114 | text-align: center;
1115 | padding-right: 10px;
1116 | }
1117 |
1118 | .bch .qr{
1119 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAABQUlEQVRo3u2ZS67DUAhDvQP2v0vvwG9wDReplTqrnuImkZqQ0wkC8wn04cAP+B5AAABKLBFASUSNNQcoSWL5PQAUq61JAI/J7gKKNdY8QHTUiEgGWJJDphQJOHEcNWVHvWbWw4ElGut8VdqHA304XY6f3lXehwNEW8/PSSECRVQUgCMdx6CRk3PFACOebAF15lRHVAZAq2YXlBs1nIKSAdBCKvuqnSYiCNA8AXMznssBjnLwFtdbZLvyZgDnxiWkI6fLSxbQlwcPeB69A2kAsMRCI6nuL5IArgjxiqb/MAGTAEw5bWQlEIOAcdTtuJas5gAziK224q5rcoA1mI+CckaQIKBXMq4lLaqrwQgBuNpup0w3n8oDbmNxl1YVCMxuxiM5i2823k8G1iK3v/jcbW4O0ELaq+w9lQUBv+/d/wX4A6YqXp+3bgF1AAAAAElFTkSuQmCC');
1120 | }
1121 | .btc .qr{
1122 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAABPUlEQVRo3u2ZTarEMAyDdQPd/5a+gWYR/yS0MLvHI5oWBur5VsaW5QT68uAH/B0QAABQCgYABgPsqA9ASQoGg0pCHXUCIoNSgMGEM2oH1O/Kki+gLVfvmbwdyMbBylIhz866HEjJjPN9Ku3lQD8BlHi8T967gVhpwqgpp3ucgHIUWSbVO1iNZASkeqTBoFJUV9qcgPpQimlnbgzn/UB2SZcMpR4sS2FcAB7qWXYj56wNEF0jAGoFGVn1AbBtHftgjeosIyCHK9tmdOEYAZwkYT6mcUyAFtGlHkCPFMoMQP/PPW+yAzIwddOneTbAsYfNYC3PaQNsi/karL2ca9vUDIDeQlM/M2WnM3cAeoy06S7TdXSWD8Ax3d1KfkDZrKyZh9JeD8xFT2ZoP831AUpIsZ/YvFyAXg787rv/C/ABz3F2h7+czu8AAAAASUVORK5CYII=');
1123 | }
1124 | .ltc .qr{
1125 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAABNklEQVRo3u2ZSwrDQAxDdQPd/5a6gbvwb0oC3ZUSdQIhOG9lbPkziA8Hf+B7gAAAYIQoAPsCQCOAERGiKEb+ZIzVCVAZI4SkuFY7oN8szhXoGMmEcQQqcZBeynOXWQ8HMK45n6vSPhyYI6RwpKTeVN5nA8JWUw47nA2wVaQajEwY5ZcTwBHPEFJGNMpqAwjlpQyUKjCjpT4AKz62wdhy4gR0jFRVbbp85wPkwNGFVJtAEzAewLabOt1VyWQDxMwcW0wmg4yAaqt6Li89VS0tnIA2cZrOjRcnIK3Y3gIc0gvgMZbikNWZ3A2Atzls5o8SkvABDqd0yz0Lq3ACGMfolfVVHE0xAmp1WX83cXgqjA8ArHxs1+UHcJZW3V9cPPlsYC96dpkrP+B9kds+24WVCfC/7/4V4AWrLcY3z6FLtwAAAABJRU5ErkJggg==');
1126 | }
1127 | .eth .qr{
1128 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAABOElEQVRo3u2ZQZLDQAgD9QP9/5f8gD2AYJzdVG6pLcvOxbHbFwokzQzyw4UH+B4QAAAwMxgMZmZwn/oAzJyXAXDBDCsgAC5WfTJP7YAAGHVDZyB7ZoIZ84EVoAaZir2ZrJsDLZlx/f1W2psDc62Ign86772BqDJNrbCeEnQCFCm4AQOyWRoBx9j0nwyOydoAmdUsZaeylKpS0AdoKyk71QRB/eIDlIZKOo8xQguIDaCoOQ6b0z1OQKx6zOz0MNEJyM5azHNNqj6yAWIF4zRWGYsPoC0aaM0hEVHgdAECuGxPtLEWaQRMwNSKXJoKJ+C6DsOrotoAL366+1ZUEjMBeAlYkHp0DDUCAkdhulLo9jEEtBrrGarsbQhsysIRt4yA40AD2qWYafIBJKSVvPcQsCgb4Dnv/i/AD+kw5hcOmeKQAAAAAElFTkSuQmCC');
1129 | }
1130 | .paypal .qr{
1131 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAABT0lEQVRo3u3aQa7DIAwE0GHFMThqOCrHYMV0YUOIFLVOvv5uskAJet24rjGk4I8LfwcNfh0tkxV54CDHnM0CYZBJkgNHMzV8IEk2gQdgAHuA09ULvAMHO/xO4DVoAFAE3oP1889kLZyPN/VB4CvYlqSxDbdrlsAXcDZEeyG47aMEvoIGePm0O5LVo0xW/y4EAoA9WUyr1dCeLOj+IYE4mG36sRQKSTYkC7VADFjlRLKZuTh561kLBZ4AIA+g9MSKPEuqPwpEATvW1udM1TEzVyAMrEO3NcjTt65qIPAAWIRnvtpg+WrhF4iCVT5nlp5dO62QCsTA1rrD2ySvpkhb0gr8BOd2kj35xnzfWAqEwXYUnMg2a4AtToUCcbC2432BS30QeA7W9KICb4Bn6XxH0eYZiEAUXI6C1/H64WcfAmFwHgXPd7nc7rJAFPz/nxM+avxtC12NiPcAAAAASUVORK5CYII=');
1132 | }
1133 | .xrp .qr{
1134 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAABRElEQVRo3u2Zy43DQAxD2YH675IdaA9DfYIYyC1YDGNfjJl3EvQhZeSHBz/gewABAAgCwcjMJKJPjYBzGAT0lWzUCjghYhDB4IlPnRoCCEYSUKBcgVTNMJKPgboeUOGoawTFvFXW5UA10tf3odPeDfRz0kTp8jR57wY0RmNNFYa+FUkPIHhup41ouFagXIBOkR6qOac2QCpB0KM1RHsBCkmJDKmLmidOAKtpnJTp1HltILcDJS+rj4RkVpTAMAF0RYmMllp7sDoAWdVy3Hmom0zhmADbgqmPSnJwVdb9wKmVWc6obJQ5ZsBUTnbkOqM8gFnPYIVLg8UIWD4sx4m0G7EB2oJz9pdj042AXuRGcjaYJcCNgL3I3XIzxrEaATnigmt95we0MS9bnoQV0D96Rmmh/akPsBe5oyy0n/ABfv+7/wvwB7fnlme6/ARZAAAAAElFTkSuQmCC');
1135 | }
1136 | #xrp .logo, .xrp .logo{
1137 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjYuNzc3IiBoZWlnaHQ9IjIyNi43NzciIHZpZXdCb3g9IjAgMCAyMjYuNzc3IDIyNi43NzciPjxwYXRoIGQ9Ik0xOTYuMjI0IDEzOS41MTVjLTcuNTktNC4xNjItMTUuODQ4LTYuMDktMjMuOTgxLTYuMDI2bC4wNjktLjAzOWMtMTEuMTU4LjEyOC0yMC4zMTEtOC41NS0yMC40NDItMTkuMzgtLjEyOS0xMC42ODYgOC41NjUtMTkuNDY3IDE5LjUwNS0xOS44MzJ2LS4wMDVsLS4wMi0uMDFjOC4yNzYtLjA0IDE2LjY0OS0yLjE0NSAyNC4yNzctNi41NDcgMjIuODEtMTMuMTY1IDMwLjMwOS00MS43ODcgMTYuNzQ5LTYzLjkzLTEzLjU2My0yMi4xNDQtNDMuMDQ3LTI5LjQyMy02NS44NTctMTYuMjU4LTIyLjgxIDEzLjE2NC0zMC4zMDggNDEuNzg3LTE2Ljc0NyA2My45MyA1LjY0OCA5LjIyNyAyLjUyNiAyMS4xNTItNi45NzggMjYuNjM4LTkuMzY5IDUuNDA3LTIxLjQzMyAyLjUzNC0yNy4xODgtNi4zODNsLS4wMDUuMDAydi4wMjVjLTQuMTQ3LTYuOTUxLTEwLjE4OS0xMi45NTYtMTcuOTEzLTE3LjE5Mi0yMy4xMDMtMTIuNjcyLTUyLjQxNy00Ljc2NC02NS40NyAxNy42NjctMTMuMDUzIDIyLjQyNi00LjkwMyA1MC44ODIgMTguMjAxIDYzLjU1MyAyMy4xMDUgMTIuNjcyIDUyLjQxNSA0Ljc2MyA2NS40NjktMTcuNjY3LjA3OS0uMTM1LjE0OS0uMjc1LjIyNi0uNDExdi4wMzNsLjAwNS4wMDJjNS41NDUtOS4wNTQgMTcuNTU1LTEyLjE5MSAyNy4wNDctNi45ODUgOS42MjggNS4yODEgMTMuMDIxIDE3LjEzNiA3LjU4MyAyNi40OC0xMy4wNTUgMjIuNDMxLTQuOTA0IDUwLjg4NSAxOC4xOTkgNjMuNTU1IDIzLjEwNCAxMi42NzMgNTIuNDE3IDQuNzYzIDY1LjQ3LTE3LjY2NyAxMy4wNTEtMjIuNDI4IDQuOTA0LTUwLjg4MS0xOC4xOTktNjMuNTUzeiIvPjwvc3ZnPg==');
1138 | }
1139 | #btc .logo, .btc .logo{
1140 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjYuNzc3IiBoZWlnaHQ9IjIyNi43NzciIHZpZXdCb3g9IjAgMCAyMjYuNzc3IDIyNi43NzciPjxwYXRoIGQ9Ik0xODIuOTgxIDExMi44NTRjLTcuMy01LjQ5OC0xNy42OTktNy42OTctMTcuNjk5LTcuNjk3czguOC01LjEwMiAxMi4zOTYtMTAuMTk5YzMuNi01LjA5OSA1LjM5OS0xMi45OTkgNS43LTE3LjA5OC4yOTktNC4xMDEgMS0yMS4yOTYtMTIuMzk5LTMxLjE5My0xMC4zNjQtNy42NTgtMjIuMjQxLTEwLjY5OC0zOC4xOS0xMS42ODdWLjI3OGgtMjEuMzk2VjM0LjU3SDk1LjA5NlYuMjc4SDczLjcwMlYzNC41N0gzMS42MXYyMi4yMTloMTIuMzcyYzMuMzczIDAgOS4zNzIuMzc1IDExLjkyMSAzLjIyOCAyLjU1IDIuODQ4IDMgNC4zNDkgMyA5Ljg5NWwuMDAxIDg4LjUzNWMwIDIuMDk5LS40IDQuNjk3LTIuMjAxIDYuMzk4LTEuNzk4IDEuNzAxLTMuNTk3IDIuMDk4LTcuODk4IDIuMDk4SDM2LjAwOWwtNC4zOTkgMjUuNjk4aDQyLjA5MnYzNC4xOTVoMjEuMzk1di0zNC4xOTVoMTYuMjk3djM0LjE5NWgyMS4zOTZ2LTM0Ljc1OWM1LjUzMS0uMzIzIDEwLjY4OC0uNzQyIDEzLjY5Ni0xLjEzNiA2LjEtLjc5OCAxOS44OTYtMi4zOTggMzIuNzk2LTExLjM5NyAxMi44OTYtOSAxNS43OTMtMjMuMDk4IDE2LjA5NC0zNy4yOTQuMzA0LTE0LjE5Ny01LjEwMi0yMy44OTctMTIuMzk1LTI5LjM5NnpNOTUuMDk2IDU4Ljc2NnM2Ljc5OC0uNTk5IDEzLjQ5Ny0uNTAxYzYuNzAxLjA5OSAxMi41OTcuMyAyMS4zOTggMyA4Ljc5NyAyLjcwMSAxMy45OTIgOS4zIDE0LjE5NiAxNy4wOTkuMTk5IDcuNzk5LTMuMjA0IDEyLjk5Ni05LjIgMTYuMjk2LTUuOTk4IDMuMjk5LTE0LjI5MiA1LjA5OS0yMi4wOTQgNS4zOTYtNy43OTcuMzAxLTE3Ljc5NyAwLTE3Ljc5NyAwdi00MS4yOXptNDcuODkgMTAyLjI3OWMtNC44OTkgMi43MDEtMTQuNjk4IDUuMS0yNC4xOTQgNS43OTgtOS40OTkuNzAxLTIzLjY5Ni40MDEtMjMuNjk2LjQwMXYtNDUuODkzczEzLjU5OC0uNjk4IDI0LjE5NyAwYzEwLjU5Ny43MDMgMTkuNDk1IDMuNCAyMy40OTIgNS40MDMgMy45OTkgMS45OTggMTEgNi4zOTYgMTEgMTYuODk2IDAgMTAuNDk2LTUuOTAzIDE0LjY5Ni0xMC43OTkgMTcuMzk1eiIvPjwvc3ZnPg==');
1141 | }
1142 | #bch .logo, .bch .logo{
1143 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjciIGhlaWdodD0iMjI3IiB2aWV3Qm94PSIwIDAgMjI3IDIyNyI+PHBhdGggZD0iTTIwMi4yNzcgMTg0aC0zOS45ODZjMjItMTUuNDIxIDM2LjM4My00MC45NTMgMzYuMzgzLTY5Ljg0IDAtMjguODg2LTE0LjM4My01NC40MTgtMzYuMzgzLTY5Ljg0aDQxLjAxOEMxODIuNTU3IDE3LjM2OSAxNDkuOTY3IDAgMTEzLjMyMiAwIDc2LjY3NCAwIDQ0LjA4NiAxNy4zNjggMjMuMzM0IDQ0LjMyaDQxLjAxOUM0Mi4zNTEgNTkuNzQzIDI3Ljk3IDg1LjI3NSAyNy45NyAxMTQuMTZjMCAyOC44ODcgMTQuMzgxIDU0LjQxOSAzNi4zODMgNjkuODRIMjQuMzY2YzIwLjc5MSAyNi4xOTggNTIuOTEyIDQzIDg4Ljk1NiA0MyAzNi4wNDMgMCA2OC4xNjQtMTYuODAyIDg4Ljk1NS00M3pNNDUuNTgyIDEzMS4xNDZjLTkuNTYtMzcuMzcgMTMuMDItNzUuNDA2IDUwLjQzMi04NC45NTQgMzcuNDEyLTkuNTUgNzUuNDg4IDEzLjAwNSA4NS4wNDcgNTAuMzc2IDkuNTU5IDM3LjM3LTEzLjAyIDc1LjQwNi01MC40MzIgODQuOTU1LTM3LjQxMSA5LjU0OC03NS40ODktMTMuMDA3LTg1LjA0Ny01MC4zNzd6Ii8+PHBhdGggZD0iTTE1MC4wODQgMTE2LjUyOWMtMS4yOTctNS41NzItNC4zNzctOC44MTgtNy43NzctMTAuMjM3cy03LjY4NC0xLjI0LTcuNjg0LTEuMjQgMi45My0yLjg2OCAzLjgyNi01LjIxN2MuODk4LTIuMzQ4LjgxMy01LjYxLjUyLTcuMjRzLTEuNzM0LTguNDEyLTcuOTU5LTEwLjkzOWMtNC44MTMtMS45NTUtOS43NTYtMS45NTctMTYuMDg4LS43NTJsLTMuNDY0LTEzLjU0NS04LjM2MSAyLjEzNCAzLjQyMyAxMy4zODQtNi4zNjcgMS42MjYtMy40MjQtMTMuMzg1LTguMzYgMi4xMzMgMy40MjUgMTMuMzg1LTE2LjQ0NyA0LjE5OCAyLjIxOCA4LjY3IDQuODMzLTEuMjMzYzEuMzItLjMzNyAzLjctLjc4OCA0Ljk4MS4wNzEgMS4yOC44NTggMS42MDcgMS4zOTggMi4xNiAzLjU2M2w4LjgzOSAzNC41NTRjLjIxLjgyMS4zMTMgMS44NzUtLjIyIDIuNzE5LS41MzQuODQyLTEuMTk2IDEuMTc3LTIuODc3IDEuNjA2bC01IDEuMjc1Ljg0OCAxMC40NyAxNi40NDYtNC4xOTggMy40MTQgMTMuMzQ3IDguMzU5LTIuMTM0LTMuNDE0LTEzLjM0NiA2LjM2Ny0xLjYyNSAzLjQxNCAxMy4zNDYgOC4zNjEtMi4xMzQtMy40NjktMTMuNTY2YzIuMTI3LS42NzggNC4xLTEuMzU0IDUuMjM2LTEuODFsLjE4LS4wNzFjMi4zODctLjk1IDcuNDU5LTIuOTY5IDExLjQ5OC03LjY0OCA0LjE0MS00Ljc5OSAzLjg2Ni0xMC41OSAyLjU2My0xNi4xNjF6bS00My4zOTItNi40NjhsLTQuMTIzLTE2LjExNXMyLjU5Ny0uOTEyIDUuMjI1LTEuNTQyYzIuNjI4LS42MjkgNC45NTEtMS4xNCA4LjY1OS0uOTYzIDMuNzA3LjE3OCA2LjM5NSAyLjIzMyA3LjI1NCA1LjI1OC44NTUgMy4wMjQuMDQ3IDUuMzkyLTEuOTY5IDcuMjc3LTIuMDE0IDEuODg3LTUuMDc0IDMuNDE1LTguMDkzIDQuMzExLTMuMDE3Ljg5NS02Ljk1MyAxLjc3NC02Ljk1MyAxLjc3NHptMjQuOCAxOS4wMjhjLTEuNjQ1IDEuNTQzLTUuMjM0IDMuNDU3LTguODczIDQuNjc3LTMuNjQzIDEuMjIyLTkuMjE5IDIuNTE5LTkuMjE5IDIuNTE5bC00LjU4Mi0xNy45MTFzNS4yNDMtMS42MjkgOS40NTQtMi40MTNjNC4yMTEtLjc4MiA3Ljk1Ny0uNjE4IDkuNzE5LS4yMzUgMS43NjIuMzgyIDQuOTM2IDEuNCA1Ljk4NCA1LjQ5OCAxLjA0OCA0LjA5NS0uODM4IDYuMzIzLTIuNDgzIDcuODY1eiIvPjwvc3ZnPg==');
1144 | }
1145 | #ltc .logo, .ltc .logo{
1146 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjYuNzc3IiBoZWlnaHQ9IjIyNi43NzciIHZpZXdCb3g9IjAgMCAyMjYuNzc3IDIyNi43NzciPjxwYXRoIGQ9Ik05NC43MTggMTg0LjE0NWwxMi43NzgtNjAuODM1IDY0LjU3OC00NC4yNzEgNy41Ny0zNi4xNTYtNjQuNTkxIDQ0LjQ1MkwxMzMuMzk4IDBoLTQ5LjYxTDU3LjE0MiAxMjcuMTg5bC0yNy4xNjcgMTguNjk4LTYuMzA4IDM0Ljg5NCAyNS45NzItMTcuODA2LTEzLjM1OCA2My43NjhoMTU4LjkxN2w4LjgyOS00Mi41OTh6Ii8+PC9zdmc+');
1147 | }
1148 | #eth .logo, .eth .logo{
1149 | background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjYuNzc3IiBoZWlnaHQ9IjIyNi43NzciIHZpZXdCb3g9IjAgMCAyMjYuNzc3IDIyNi43NzciPjxwYXRoIGQ9Ik0xMTIuNTUzIDE1N1Y4Ni45NzdsLTY4LjM5NSAyOS45NnptMC03NC44MzdWLS4wNTZMNDYuMzYyIDExMS4xNTZ6TTExNi45NjItLjA5djgyLjI1M2w2Ny4xMjEgMjkuNDAzem0wIDg3LjA2N3Y3MC4wMjVsNjguNDQzLTQwLjA0NXptLTQuNDA5IDE0MC40Mjl2LTU2LjMyMUw0NC42MTggMTMxLjMxem00LjQwOSAwbDY3LjkzNS05Ni4wOTYtNjcuOTM1IDM5Ljc3NXoiLz48L3N2Zz4=');
1150 | }
1151 | #paypal .logo, .paypal .logo{
1152 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAEvCAMAAACpLy6rAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAjboAAI26AU8yY2QAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMjHxIGmVAAAC+lBMVEX///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABawBz3AAAA/XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFpcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+qfsm2AAADjtJREFUGBntwQuczXXeB/DPmasxZszkFo+KVG6VnYpU2wolWSHZii6r8qSprc3KI0pyqSTF2jxbujf1FDbSpBstS1qrSWI265IUSqjRDDPOzPm8Xg9yfmcuv3Nm5vf9n/+c37z+7zc8Ho/H4/F4PB6Px+PxeDwej8fjCUrMcF9aHGLfGNaBQOF3mz9b/spDN17cyocY9Sbr1sG1M3/XEjFoF2PA1hdvbo7YcjJjROmykS0QQ65h7Cj76PYMxIoZjCk/zzwVsWEVY0zZgosQAxIPMfb883LUua6MSbkdUMfuZGzyz8xEnXqFsWrfHXGoQ1sYu5a2Qp1pylj2w5WoK/0Z22Ylo25MYoz7/AzUiQ8Y6344F3UgroAx70APuK8zLXDoSrhuOG3gHwa3PU0rBG6Cy9bTDod7wlWNymiJ/e3hpp60xpYmcNFY2mNFEtyziBZ5Hu75jjYZCLe0oVV2NoZLrqNd5sIlT9IyveGO1bTMtlS4IamYtpkGN5xP6xQ1hQvuon0mwQWv0j4/piP6ttFCYxB1zWmj7xog2gbQStmItim00gZE21LaqT2iK+4A7XQfoussWupTRNcttFVbRNVc2moUomoDbbUa0ZReRlsFTkAU9aa9eiOKxtFeoxBFiymXv64aX+wsZhS8gijaQ7HiZFQvtd3Qp74I0FEbED2nUu5j1FSrCTvpoNIURM1Qyk1HzSUM+ZLO6YaomUW5waiN5IkldMoIlNcODlpDuZaonQ7r6ZDpKO+ynnBMcgnFvkJtNV5GZ8xFeQmfN4dTLqBcDmot6TU6Yj4qeOpdHxzyR8plo/bi36MTPkQFF3MMHPI65bJgIHMzHbAWFfh2+LPgjO0UK4yHiY6FlNuCih7jCjjiRMotg5lxlPsBFZ1D/g5OGES5yTDT4GuK+VHJJm5vAAc8TLl+MHQd5RqiosnkeDjgI4oFMmHI9xXFklDRVWThf0EsvpBi+TA2iVLFqKQ9yT9DrAvlnoVOyonVaBEPnEGpPagkoYQsaASpEZS7GToPsTolGxdM+YFCW1DZFyRHQuo5ynWEzod0RR4qe53kBkjlU2y/DxrxB+iKv6OyB3jEJZBpHKBYLnS60B2LUdkQHjEPMpdRbhx0RtIdOaisE4/wN4PI/ZTrBZ2X6Y45qCzxMI+4HiK5FCtNhc5WumM8qsjnEa9CZC/F8qDTgi4ZjCre5RF74yBwGuVmQ+cquqQDqljCo7pDYBjlhkLnMbrjcAKqeIdHTYTAbMq1hc4qumMjqsrlUWsgsJZiu6GTVEx3zENVuTyqLB3GUg5TbAF0ulP54ZNI8inzEKp6m8d0g7GLKDcKOvdQeQCRDKPMdahqMY+5EcZGUe4C6MyncikimUyZs1HVYh7zMIzNo1hxMnR2MagsDZHMp0hpMqp6i8cshLEdFPsYOm2ofI5I4nZT5DNoLOIxm2CqFeWmQ2colTmI5DeUeRwai3iMPxGGBlNuMHT+TOUGRDKLMv2hsZC/6AhD0yh3InQ+pXIaIvB9S5HSdGgs5y96wNByim2DTmopg/Ygkgsp8wl0dvEX/WAmoYhiOdDpSWURIplBmYehkcrjhsBMFuWyoTOOyhhEkLyTMpdBowuPuxFmRlIuCzq5VH6DCO6mTElDaAzhcbfDzAsUK4yHhm8fgw6nILy0PZRZAZ2xPG40zHxJsaXQ6UBlDSKYQKFR0HmOxz0II5kBik2GznAqMxFe0wOUCbSGzj943GMw0pdy/aAzl8q1CG8GhZZDazePmwojEygWyIROPpWTEFbnYgrdBp00Bt0LI0solg+dzACDvkVYqfkU8jeFThaDboUJ336KzYVOPypvIKwXKbUEWtcwaAhMtKfccOhMpnI3wrmJYjdAaxyDLoWJGyjXETrLqJyPMDoVUepQGrTmM+hcmHiKYvt90IgvZNChJOg13kixHGj59jKoHUzkUSwXOllUVkIvYw3lzoPWr6icAAMN/RQbB51sKtOglbmWciugdw+DyuJh4GLK9YRODpVB0GmSRwcMhN5iBm2GidEUK02FzldUWkCj6To6YHMctOILGLQQJhZQLA86LalshUaHjXRCNvTOpzIVJr6l2GzoXE3lZVQ1vIhO2NcQemOpDIOB1pQbCp3pVG5HZemv0RlTEcYHVM6BgSGUawOdj6n8CpV020pn/NwCehklDCpLgYHpFNsFneQSBv0cjwqazThMh0xAGMOpbIWJf1BsAXQupLIU5WVOKaRTdqcijPepLIaBhIMUGwWdP1GZhJC0+3+ic0YgjOalVB6FgXMpdwF0/kblChwX32v2XjooPx5hZDPkKhjIplhxMnR2MyiQiaOSf/vsXjprAMJZSaUsEwZeotgq6JxKJf+ErtfeN3fZATptOcI5OUDlU5j4D8UKvtTZwagr64pwRjNkOgw0ocWmIaz1DOkHA/1or43JCKc3Q/xpMDCR1vKfh7DeZshqmHiP1noIYbUPMGQqDPh+pK0+S0RYc1jOpTDQkbYqORthnVDEkH2JMHATbTUa4Y1lOU/DxP/SUq8jvMSdLKcnTKyjndY1RHi3sJydcTCQWkor7W2D8FJ3sZwnYKIHreTvhQgmsLxuMDGGVroLEbQsZDlbYeRN2uh5RPIMy5sCI7tpoTcTEMGZZSwncDpMnEwL5SYhkndZ3iIYuYb2eT8ZkfRlBT1gZAat81EKIkn/muV9CjMf0zYrUxHRs6xgKIwkHqJlVqUjon6s4JsEGOlKy7zRABFl7mIF98LMnbTLIz5ElsMKfs6AmRzaxH8rqjGYFU2FoS20SEEfVKPlHlawJx1mmtEi289ENZJXs6JsGOpPeyzIRHWeZUWbEmBoMm1RNALVuoOVDIKpD2iJdR1QrR5+VrQCpuIKaIXAE8mo1il7WFGgG0x1phW+vgLVa5jHSl6BseG0QPGkhqhegw9YyXdNYOxpxr63TkUNJL3DygbC3HrGus39UBOJC1nZyzCXVsbYtn9sMmoifh4r25kJcz0Z077/nzTUSFwOq/gtBMYyhn17VwpqJv4FVvEcJBYxZm0bkYQaapTLKrY0hsR3jE3+RYMSUFOt8ljFgc6QaMOYtGFUc9TcWTtYRWAgRK5j7Nn/l66ojT4FrOp+yDzJGPPvx3snoVZu8bOq+T7IfMIYcjA3uy1qqdHz1Pg8FTJJxYwRO94cf3kKaq3rZmrsaQuh81n3yrb/bVzfZjARN9ZPjX1dIHUX60qgaO+/33t6/A092ibCVOuPqPPTuRB7jWJbutdSt7NOa5XZAHJD91PnQHfIbaPYS6gbZ/6dWoUXQ6455bJRFxrP9FPrYC84YADlzoP7fL//nno/94ETplKsOBGuu/AThrEzC45YSrHVcFvPpQzn89ZwRNwBij0Jd/X7mGEtSYMzzqLcULjId3Uew5sTD4fcSrl2cM2J925ieKWj4Ji5FNsLlyQMesvPCL7+NZyzgWLvwBWdpn/PiN7IgHPSyyg2AVGXeMmj6xlZ4c1w0qWUuwLR1fqWBQWszqdnwFHjKNcEURN/5k2z17N6JVOS4KzFFNuCqIjreP3MVUWskSWnw2l7KJYDJyU1Obv/yIdfWbHdz5raNgCOa0e5P0Bn4ru1tOyTL7buLvCztg4+0ADOG0q57tDZQVeU5pyCaJhFsZJkaLSkG0qeOQ3RsYZi/4LOQEZf0ZOtESUNDlNsNnSmMtp+mtIMUXMB5a6HzlJG18rhjRBFf6Rce2jEFTCKdj/aHtH1OsV+9EGjE6Pm8MIBCYi27RR7Hzo3MTp2/HVQGqLvRMpNgs4cOq/kwz91hjsGUa4/dPLorO3zx/RuBNc8Qrnm0Ejx0ynFmxeN79sU7vqIYl9B5yKK+H/atWX9qjem33XVec19cF98IcVeh849VO5sUzuntGyciDrWhXL3QOf/qJwG+/w35S6CzlcM2gcLPUcxfwo0mlNZAgvlUywPOv2pTIR9MgIUmwOdSVT6wT59KPd76LxHpSnscz/lOkHD9yODtsJCuRQriINGeyqvwkJ7KbYUOtdTuRv2OZ1yU6Ezm8oFsM8wyg2Czr8YdLgB7DObci2hkVzCoLWw0FqKfQOd7lT+AvukHKbYfOj8gcqNsM9FlBsNnRwqHWCfUZTrAZ0tDPrJB/vMo1hpKjSaUPkAFvqGYuuhcwWVybBPK8o9A50HqVwJ+wym3C3QeYdKC9hnGuXOhs5eBm2HhVZQrDAeGu2ovAH7JBRRbDl0hlIZBftkUW4adGZS+TXsM5JyV0NnNYP8DWGfFyl3EjSSihn0GSz0JcV2Qacrlb/CPpkBir0JnWwqw2GfvpQbC52XqHSGfR6kXC/obGLQgTjYZwnFytKhkRFg0DLYx7efYhuh04fKI7BPe8o9B53xVK6CfW6k3G3QWUylFezzFOWyoPM9g76FhfIodjABGm2oLIB9GvopthI611IZA/tcTLnHoTODyiWwz2jKXQudlQwqawT7LKBcG2gkHGTQF7DQTop9D50sKnNhn9aUWwyd26jcCvsModx46DxPpQvsM51yfaCzkUFF8bDPSooFMqCRXsagFbBP4kGKbYJObyqPwT7nUu4l6IylMgT2yaZcNnQWUjkZ9mnWQS4VOrsYtBuekJOoLIInZAiV++AJmUalNzwhyxkUaAyPEl/IoHx4Qs6m8gI8IbdSGQlPyFwq58ATsp5BhxLhURqVMWgVPCE9qDwBT8i9VK6DJ2QBlbbwhHzDoD3whLSi8jbqoT7rDG2m8gDqoUcpdznqoeUUC2Si/kkooth/UA9lUe5l1EMjKXcH6qEXKdcN9dCXFCtJQv2TGaDYP1EP9aXcLNRDD1JuGOqhdyl3Ouof348U24f6KEMuDR6Px+PxeDwej8fj8Xg8Ho+ndv4fD0GG7trOAJ8AAAAASUVORK5CYII=');
1153 | }
1154 |
--------------------------------------------------------------------------------
/www/alarm/custom-element.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Home Assistant
6 |
7 |
10 |
11 |
12 |
Lounge
13 |

14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/www/alarm/donate/bch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/donate/bch.png
--------------------------------------------------------------------------------
/www/alarm/donate/btc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/donate/btc.png
--------------------------------------------------------------------------------
/www/alarm/donate/eth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/donate/eth.png
--------------------------------------------------------------------------------
/www/alarm/donate/ltc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/donate/ltc.png
--------------------------------------------------------------------------------
/www/alarm/donate/paypal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/donate/paypal.png
--------------------------------------------------------------------------------
/www/alarm/donate/xrp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/donate/xrp.png
--------------------------------------------------------------------------------
/www/alarm/lobster.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/alarm/lobster.woff2
--------------------------------------------------------------------------------
/www/images/camera-garage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/images/camera-garage.jpg
--------------------------------------------------------------------------------
/www/images/camera-outdoor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/images/camera-outdoor.jpg
--------------------------------------------------------------------------------
/www/images/camera-pool.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/images/camera-pool.jpg
--------------------------------------------------------------------------------
/www/images/camera-tv-room.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/images/camera-tv-room.jpg
--------------------------------------------------------------------------------
/www/images/gazos.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/images/gazos.jpg
--------------------------------------------------------------------------------
/www/images/ha.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gazoscalvertos/Hass-Custom-Alarm/2dd16d6725458ebc8ef8bfd73f1ed062c63803a3/www/images/ha.png
--------------------------------------------------------------------------------
/www/lib/countdown360.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Countdown 360 - v0.1.9
3 | * This is a simple attractive circular countdown timer that counts down a number of seconds. The style is configurable and callbacks are supported on completion.
4 | * https://github.com/johnschult/jquery.countdown360
5 | *
6 | * Made by John Schult
7 | * Under MIT License
8 | */
9 | (function ($, window, document, undefined) {
10 | var pluginName = "countdown360",
11 | defaults = {
12 | radius: 15.5, // radius of arc
13 | strokeStyle: "#477050", // the color of the stroke
14 | strokeWidth: undefined, // the stroke width, dynamically calulated if omitted in options
15 | fillStyle: "#8ac575", // the fill color
16 | fillStyle_0to50: "#8ac575", // the beginning of the timer 0 to 50%
17 | fillStyle_50to75: "orange", // the middle of the timer 50 to 75%
18 | fillStyle_75to100: "red", // the end of the timer 75 to 100%
19 | fontColor: "#477050", // the font color
20 | fontFamily: "sans-serif", // the font family
21 | fontSize: undefined, // the font size, dynamically calulated if omitted in options
22 | fontWeight: 700, // the font weight
23 | autostart: true, // start the countdown automatically
24 | seconds: 10, // the number of seconds to count down
25 | label: ["second", "seconds"], // the label to use or false if none
26 | startOverAfterAdding: true, // Start the timer over after time is added with addSeconds
27 | smooth: false, // should the timer be smooth or stepping
28 | onComplete: function () {}
29 | };
30 |
31 | function Plugin(element, options) {
32 | this.element = element;
33 | this.settings = $.extend({}, defaults, options);
34 | if (!this.settings.fontSize) { this.settings.fontSize = this.settings.radius/1.2; }
35 | if (!this.settings.strokeWidth) { this.settings.strokeWidth = this.settings.radius/4; }
36 | this._defaults = defaults;
37 | this._name = pluginName;
38 | this._init();
39 | }
40 |
41 | Plugin.prototype = {
42 |
43 | getTimeRemaining: function()
44 | {
45 | var timeRemaining = this._secondsLeft(this.getElapsedTime());
46 | return timeRemaining;
47 | },
48 | getElapsedTime: function()
49 | {
50 | return Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
51 | },
52 | extendTimer: function (value) {
53 | var seconds = parseInt(value),
54 | secondsElapsed = Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
55 | if ((this._secondsLeft(secondsElapsed) + seconds) <= this.settings.seconds) {
56 | this.startedAt.setSeconds(this.startedAt.getSeconds() + parseInt(value));
57 | }
58 | },
59 | addSeconds: function (value) {
60 | var secondsElapsed = Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
61 | if (this.settings.startOverAfterAdding) {
62 | this.settings.seconds = this._secondsLeft(secondsElapsed) + parseInt(value);
63 | this.start();
64 | } else {
65 | this.settings.seconds += parseInt(value);
66 | }
67 | },
68 |
69 | start: function () {
70 | this.startedAt = new Date();
71 | this._drawCountdownShape(Math.PI*3.5, true);
72 | this._drawCountdownLabel(0);
73 | var timerInterval = 1000;
74 | if (this.settings.smooth) {
75 | timerInterval = 16;
76 | }
77 | this.interval = setInterval(jQuery.proxy(this._draw, this), timerInterval);
78 | },
79 |
80 | stop: function (cb) {
81 | clearInterval(this.interval);
82 | if (cb) { cb(); }
83 | },
84 |
85 | _init: function () {
86 | this.settings.width = (this.settings.radius * 2) + (this.settings.strokeWidth * 2);
87 | this.settings.height = this.settings.width;
88 | this.settings.arcX = this.settings.radius + this.settings.strokeWidth;
89 | this.settings.arcY = this.settings.arcX;
90 | this._initPen(this._getCanvas());
91 | if (this.settings.autostart) { this.start(); }
92 | },
93 |
94 | _getCanvas: function () {
95 | var $canvas = $("");
99 | $(this.element).prepend($canvas[0]);
100 | return $canvas[0];
101 | },
102 |
103 | _initPen: function (canvas) {
104 | this.pen = canvas.getContext("2d");
105 | this.pen.lineWidth = this.settings.strokeWidth;
106 | this.pen.strokeStyle = this.settings.strokeStyle;
107 | this.pen.fillStyle = this.settings.fillStyle;
108 | this.pen.textAlign = "center";
109 | this.pen.textBaseline = "middle";
110 | this.ariaText = $(canvas).children("#countdown-text");
111 | this._clearRect();
112 | },
113 |
114 | _clearRect: function () {
115 | this.pen.clearRect(0, 0, this.settings.width, this.settings.height);
116 | },
117 |
118 | _secondsLeft: function(secondsElapsed) {
119 | return this.settings.seconds - secondsElapsed;
120 | },
121 |
122 | _drawCountdownLabel: function (secondsElapsed) {
123 | this.ariaText.text(secondsLeft);
124 | this.pen.font = this.settings.fontWeight + " " + this.settings.fontSize + "px " + this.settings.fontFamily;
125 | var secondsLeft = this._secondsLeft(secondsElapsed),
126 | label = secondsLeft === 1 ? this.settings.label[0] : this.settings.label[1],
127 | drawLabel = this.settings.label && this.settings.label.length === 2,
128 | x = this.settings.width/2;
129 | if (drawLabel) {
130 | y = this.settings.height/2 - (this.settings.fontSize/6.2);
131 | } else {
132 | y = this.settings.height/2;
133 | }
134 | this.pen.fillStyle = this.settings.fillStyle;
135 | this.pen.fillText(secondsLeft + 1, x, y);
136 | this.pen.fillStyle = this.settings.fontColor;
137 | this.pen.fillText(secondsLeft, x, y);
138 | if (drawLabel) {
139 | this.pen.font = "normal small-caps " + (this.settings.fontSize/3) + "px " + this.settings.fontFamily;
140 | this.pen.fillText(label, this.settings.width/2, this.settings.height/2 + (this.settings.fontSize/2.2));
141 | }
142 | },
143 |
144 | _drawCountdownShape: function (endAngle, drawStroke) {
145 | this.pen.fillStyle = this.settings.fillStyle;
146 | this.pen.beginPath();
147 | this.pen.arc(this.settings.arcX, this.settings.arcY, this.settings.radius, Math.PI*1.5, endAngle, false);
148 | this.pen.fill();
149 | if (drawStroke) { this.pen.stroke(); }
150 | },
151 | _draw: function () {
152 | var millisElapsed, secondsElapsed;
153 | millisElapsed = new Date().getTime() - this.startedAt.getTime();
154 | secondsElapsed = Math.floor((millisElapsed)/1000);
155 | progress = secondsElapsed / this.settings.seconds;
156 |
157 | if (progress > 0.75) {
158 | this.settings.fillStyle = this.settings.fillStyle_75to100;
159 | } else if (progress > 0.5 ){
160 | this.settings.fillStyle = this.settings.fillStyle_50to75;
161 | } else {
162 | this.settings.fillStyle =this.settings.fillStyle_0to50;
163 | }
164 |
165 | endAngle = (Math.PI*3.5) - (((Math.PI*2)/(this.settings.seconds * 1000)) * millisElapsed);
166 | this._clearRect();
167 | this._drawCountdownShape(Math.PI*3.5, false);
168 | if (secondsElapsed < this.settings.seconds) {
169 | this._drawCountdownShape(endAngle, true);
170 | this._drawCountdownLabel(secondsElapsed);
171 | } else {
172 | this._drawCountdownLabel(this.settings.seconds);
173 | this.stop();
174 | this.settings.onComplete();
175 | }
176 | }
177 |
178 | };
179 |
180 | $.fn[pluginName] = function (options) {
181 | var plugin = null;
182 | this.each(function() {
183 | plugin = $.data(this, "plugin_" + pluginName);
184 | if (!plugin) {
185 | plugin = new Plugin(this, options);
186 | $.data(this, "plugin_" + pluginName, plugin);
187 | }
188 | });
189 | return plugin;
190 | };
191 |
192 | })(jQuery, window, document);
193 |
--------------------------------------------------------------------------------
/www/lib/jscolor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jscolor - JavaScript Color Picker
3 | *
4 | * @link http://jscolor.com
5 | * @license For open source use: GPLv3
6 | * For commercial use: JSColor Commercial License
7 | * @author Jan Odvarko
8 | * @version 2.0.5
9 | *
10 | * See usage examples at http://jscolor.com/examples/
11 | */
12 |
13 |
14 | "use strict";
15 |
16 |
17 | if (!window.jscolor) { window.jscolor = (function () {
18 |
19 |
20 | var jsc = {
21 |
22 |
23 | register : function () {
24 | jsc.attachDOMReadyEvent(jsc.init);
25 | jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown);
26 | jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart);
27 | jsc.attachEvent(window, 'resize', jsc.onWindowResize);
28 | },
29 |
30 |
31 | init : function () {
32 | if (jsc.jscolor.lookupClass) {
33 | jsc.jscolor.installByClassName(jsc.jscolor.lookupClass);
34 | }
35 | },
36 |
37 |
38 | tryInstallOnElements : function (elms, className) {
39 | var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i');
40 |
41 | for (var i = 0; i < elms.length; i += 1) {
42 | if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') {
43 | if (jsc.isColorAttrSupported) {
44 | // skip inputs of type 'color' if supported by the browser
45 | continue;
46 | }
47 | }
48 | var m;
49 | if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) {
50 | console.log('test');
51 | var targetElm = elms[i];
52 | var optsStr = null;
53 |
54 | var dataOptions = jsc.getDataAttr(targetElm, 'jscolor');
55 | if (dataOptions !== null) {
56 | optsStr = dataOptions;
57 | } else if (m[4]) {
58 | optsStr = m[4];
59 | }
60 |
61 | var opts = {};
62 | if (optsStr) {
63 | try {
64 | opts = (new Function ('return (' + optsStr + ')'))();
65 | } catch(eParseError) {
66 | jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr);
67 | }
68 | }
69 | targetElm.jscolor = new jsc.jscolor(targetElm, opts);
70 | }
71 | }
72 | },
73 |
74 |
75 | isColorAttrSupported : (function () {
76 | var elm = document.createElement('input');
77 | if (elm.setAttribute) {
78 | elm.setAttribute('type', 'color');
79 | if (elm.type.toLowerCase() == 'color') {
80 | return true;
81 | }
82 | }
83 | return false;
84 | })(),
85 |
86 |
87 | isCanvasSupported : (function () {
88 | var elm = document.createElement('canvas');
89 | return !!(elm.getContext && elm.getContext('2d'));
90 | })(),
91 |
92 |
93 | fetchElement : function (mixed) {
94 | return typeof mixed === 'string' ? document.getElementById(mixed) : mixed;
95 | },
96 |
97 |
98 | isElementType : function (elm, type) {
99 | return elm.nodeName.toLowerCase() === type.toLowerCase();
100 | },
101 |
102 |
103 | getDataAttr : function (el, name) {
104 | var attrName = 'data-' + name;
105 | var attrValue = el.getAttribute(attrName);
106 | if (attrValue !== null) {
107 | return attrValue;
108 | }
109 | return null;
110 | },
111 |
112 |
113 | attachEvent : function (el, evnt, func) {
114 | if (el.addEventListener) {
115 | el.addEventListener(evnt, func, false);
116 | } else if (el.attachEvent) {
117 | el.attachEvent('on' + evnt, func);
118 | }
119 | },
120 |
121 |
122 | detachEvent : function (el, evnt, func) {
123 | if (el.removeEventListener) {
124 | el.removeEventListener(evnt, func, false);
125 | } else if (el.detachEvent) {
126 | el.detachEvent('on' + evnt, func);
127 | }
128 | },
129 |
130 |
131 | _attachedGroupEvents : {},
132 |
133 |
134 | attachGroupEvent : function (groupName, el, evnt, func) {
135 | if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
136 | jsc._attachedGroupEvents[groupName] = [];
137 | }
138 | jsc._attachedGroupEvents[groupName].push([el, evnt, func]);
139 | jsc.attachEvent(el, evnt, func);
140 | },
141 |
142 |
143 | detachGroupEvents : function (groupName) {
144 | if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) {
145 | for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) {
146 | var evt = jsc._attachedGroupEvents[groupName][i];
147 | jsc.detachEvent(evt[0], evt[1], evt[2]);
148 | }
149 | delete jsc._attachedGroupEvents[groupName];
150 | }
151 | },
152 |
153 |
154 | attachDOMReadyEvent : function (func) {
155 | var fired = false;
156 | var fireOnce = function () {
157 | if (!fired) {
158 | fired = true;
159 | func();
160 | }
161 | };
162 |
163 | if (document.readyState === 'complete') {
164 | setTimeout(fireOnce, 1); // async
165 | return;
166 | }
167 |
168 | if (document.addEventListener) {
169 | document.addEventListener('DOMContentLoaded', fireOnce, false);
170 |
171 | // Fallback
172 | window.addEventListener('load', fireOnce, false);
173 |
174 | } else if (document.attachEvent) {
175 | // IE
176 | document.attachEvent('onreadystatechange', function () {
177 | if (document.readyState === 'complete') {
178 | document.detachEvent('onreadystatechange', arguments.callee);
179 | fireOnce();
180 | }
181 | })
182 |
183 | // Fallback
184 | window.attachEvent('onload', fireOnce);
185 |
186 | // IE7/8
187 | if (document.documentElement.doScroll && window == window.top) {
188 | var tryScroll = function () {
189 | if (!document.body) { return; }
190 | try {
191 | document.documentElement.doScroll('left');
192 | fireOnce();
193 | } catch (e) {
194 | setTimeout(tryScroll, 1);
195 | }
196 | };
197 | tryScroll();
198 | }
199 | }
200 | },
201 |
202 |
203 | warn : function (msg) {
204 | if (window.console && window.console.warn) {
205 | window.console.warn(msg);
206 | }
207 | },
208 |
209 |
210 | preventDefault : function (e) {
211 | if (e.preventDefault) { e.preventDefault(); }
212 | e.returnValue = false;
213 | },
214 |
215 |
216 | captureTarget : function (target) {
217 | // IE
218 | if (target.setCapture) {
219 | jsc._capturedTarget = target;
220 | jsc._capturedTarget.setCapture();
221 | }
222 | },
223 |
224 |
225 | releaseTarget : function () {
226 | // IE
227 | if (jsc._capturedTarget) {
228 | jsc._capturedTarget.releaseCapture();
229 | jsc._capturedTarget = null;
230 | }
231 | },
232 |
233 |
234 | fireEvent : function (el, evnt) {
235 | if (!el) {
236 | return;
237 | }
238 | if (document.createEvent) {
239 | var ev = document.createEvent('HTMLEvents');
240 | ev.initEvent(evnt, true, true);
241 | el.dispatchEvent(ev);
242 | } else if (document.createEventObject) {
243 | var ev = document.createEventObject();
244 | el.fireEvent('on' + evnt, ev);
245 | } else if (el['on' + evnt]) { // alternatively use the traditional event model
246 | el['on' + evnt]();
247 | }
248 | },
249 |
250 |
251 | classNameToList : function (className) {
252 | return className.replace(/^\s+|\s+$/g, '').split(/\s+/);
253 | },
254 |
255 |
256 | // The className parameter (str) can only contain a single class name
257 | hasClass : function (elm, className) {
258 | if (!className) {
259 | return false;
260 | }
261 | return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' ');
262 | },
263 |
264 |
265 | // The className parameter (str) can contain multiple class names separated by whitespace
266 | setClass : function (elm, className) {
267 | var classList = jsc.classNameToList(className);
268 | for (var i = 0; i < classList.length; i += 1) {
269 | if (!jsc.hasClass(elm, classList[i])) {
270 | elm.className += (elm.className ? ' ' : '') + classList[i];
271 | }
272 | }
273 | },
274 |
275 |
276 | // The className parameter (str) can contain multiple class names separated by whitespace
277 | unsetClass : function (elm, className) {
278 | var classList = jsc.classNameToList(className);
279 | for (var i = 0; i < classList.length; i += 1) {
280 | var repl = new RegExp(
281 | '^\\s*' + classList[i] + '\\s*|' +
282 | '\\s*' + classList[i] + '\\s*$|' +
283 | '\\s+' + classList[i] + '(\\s+)',
284 | 'g'
285 | );
286 | elm.className = elm.className.replace(repl, '$1');
287 | }
288 | },
289 |
290 |
291 | getStyle : function (elm) {
292 | return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle;
293 | },
294 |
295 |
296 | setStyle : (function () {
297 | var helper = document.createElement('div');
298 | var getSupportedProp = function (names) {
299 | for (var i = 0; i < names.length; i += 1) {
300 | if (names[i] in helper.style) {
301 | return names[i];
302 | }
303 | }
304 | };
305 | var props = {
306 | borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']),
307 | boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow'])
308 | };
309 | return function (elm, prop, value) {
310 | switch (prop.toLowerCase()) {
311 | case 'opacity':
312 | var alphaOpacity = Math.round(parseFloat(value) * 100);
313 | elm.style.opacity = value;
314 | elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')';
315 | break;
316 | default:
317 | elm.style[props[prop]] = value;
318 | break;
319 | }
320 | };
321 | })(),
322 |
323 |
324 | setBorderRadius : function (elm, value) {
325 | jsc.setStyle(elm, 'borderRadius', value || '0');
326 | },
327 |
328 |
329 | setBoxShadow : function (elm, value) {
330 | jsc.setStyle(elm, 'boxShadow', value || 'none');
331 | },
332 |
333 |
334 | getElementPos : function (e, relativeToViewport) {
335 | var x=0, y=0;
336 | var rect = e.getBoundingClientRect();
337 | x = rect.left;
338 | y = rect.top;
339 | if (!relativeToViewport) {
340 | var viewPos = jsc.getViewPos();
341 | x += viewPos[0];
342 | y += viewPos[1];
343 | }
344 | return [x, y];
345 | },
346 |
347 |
348 | getElementSize : function (e) {
349 | return [e.offsetWidth, e.offsetHeight];
350 | },
351 |
352 |
353 | // get pointer's X/Y coordinates relative to viewport
354 | getAbsPointerPos : function (e) {
355 | if (!e) { e = window.event; }
356 | var x = 0, y = 0;
357 | if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) {
358 | // touch devices
359 | x = e.changedTouches[0].clientX;
360 | y = e.changedTouches[0].clientY;
361 | } else if (typeof e.clientX === 'number') {
362 | x = e.clientX;
363 | y = e.clientY;
364 | }
365 | return { x: x, y: y };
366 | },
367 |
368 |
369 | // get pointer's X/Y coordinates relative to target element
370 | getRelPointerPos : function (e) {
371 | if (!e) { e = window.event; }
372 | var target = e.target || e.srcElement;
373 | var targetRect = target.getBoundingClientRect();
374 |
375 | var x = 0, y = 0;
376 |
377 | var clientX = 0, clientY = 0;
378 | if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) {
379 | // touch devices
380 | clientX = e.changedTouches[0].clientX;
381 | clientY = e.changedTouches[0].clientY;
382 | } else if (typeof e.clientX === 'number') {
383 | clientX = e.clientX;
384 | clientY = e.clientY;
385 | }
386 |
387 | x = clientX - targetRect.left;
388 | y = clientY - targetRect.top;
389 | return { x: x, y: y };
390 | },
391 |
392 |
393 | getViewPos : function () {
394 | var doc = document.documentElement;
395 | return [
396 | (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
397 | (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
398 | ];
399 | },
400 |
401 |
402 | getViewSize : function () {
403 | var doc = document.documentElement;
404 | return [
405 | (window.innerWidth || doc.clientWidth),
406 | (window.innerHeight || doc.clientHeight),
407 | ];
408 | },
409 |
410 |
411 | redrawPosition : function () {
412 |
413 | if (jsc.picker && jsc.picker.owner) {
414 | var thisObj = jsc.picker.owner;
415 |
416 | var tp, vp;
417 |
418 | if (thisObj.fixed) {
419 | // Fixed elements are positioned relative to viewport,
420 | // therefore we can ignore the scroll offset
421 | tp = jsc.getElementPos(thisObj.targetElement, true); // target pos
422 | vp = [0, 0]; // view pos
423 | } else {
424 | tp = jsc.getElementPos(thisObj.targetElement); // target pos
425 | vp = jsc.getViewPos(); // view pos
426 | }
427 |
428 | var ts = jsc.getElementSize(thisObj.targetElement); // target size
429 | var vs = jsc.getViewSize(); // view size
430 | var ps = jsc.getPickerOuterDims(thisObj); // picker size
431 | var a, b, c;
432 | switch (thisObj.position.toLowerCase()) {
433 | case 'left': a=1; b=0; c=-1; break;
434 | case 'right':a=1; b=0; c=1; break;
435 | case 'top': a=0; b=1; c=-1; break;
436 | default: a=0; b=1; c=1; break;
437 | }
438 | var l = (ts[b]+ps[b])/2;
439 |
440 | // compute picker position
441 | if (!thisObj.smartPosition) {
442 | var pp = [
443 | tp[a],
444 | tp[b]+ts[b]-l+l*c
445 | ];
446 | } else {
447 | var pp = [
448 | -vp[a]+tp[a]+ps[a] > vs[a] ?
449 | (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) :
450 | tp[a],
451 | -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ?
452 | (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) :
453 | (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c)
454 | ];
455 | }
456 |
457 | var x = pp[a];
458 | var y = pp[b];
459 | var positionValue = thisObj.fixed ? 'fixed' : 'absolute';
460 | var contractShadow =
461 | (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) &&
462 | (pp[1] + ps[1] < tp[1] + ts[1]);
463 |
464 | jsc._drawPosition(thisObj, x, y, positionValue, contractShadow);
465 | }
466 | },
467 |
468 |
469 | _drawPosition : function (thisObj, x, y, positionValue, contractShadow) {
470 | var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px
471 |
472 | jsc.picker.wrap.style.position = positionValue;
473 | jsc.picker.wrap.style.left = x + 'px';
474 | jsc.picker.wrap.style.top = y + 'px';
475 |
476 | jsc.setBoxShadow(
477 | jsc.picker.boxS,
478 | thisObj.shadow ?
479 | new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) :
480 | null);
481 | },
482 |
483 |
484 | getPickerDims : function (thisObj) {
485 | var displaySlider = !!jsc.getSliderComponent(thisObj);
486 | var dims = [
487 | 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width +
488 | (displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0),
489 | 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height +
490 | (thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0)
491 | ];
492 | return dims;
493 | },
494 |
495 |
496 | getPickerOuterDims : function (thisObj) {
497 | var dims = jsc.getPickerDims(thisObj);
498 | return [
499 | dims[0] + 2 * thisObj.borderWidth,
500 | dims[1] + 2 * thisObj.borderWidth
501 | ];
502 | },
503 |
504 |
505 | getPadToSliderPadding : function (thisObj) {
506 | return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness));
507 | },
508 |
509 |
510 | getPadYComponent : function (thisObj) {
511 | switch (thisObj.mode.charAt(1).toLowerCase()) {
512 | case 'v': return 'v'; break;
513 | }
514 | return 's';
515 | },
516 |
517 |
518 | getSliderComponent : function (thisObj) {
519 | if (thisObj.mode.length > 2) {
520 | switch (thisObj.mode.charAt(2).toLowerCase()) {
521 | case 's': return 's'; break;
522 | case 'v': return 'v'; break;
523 | }
524 | }
525 | return null;
526 | },
527 |
528 |
529 | onDocumentMouseDown : function (e) {
530 |
531 | if (!e) { e = window.event; }
532 | var target = e.explicitOriginalTarget || e.path[0];
533 | if (target._jscLinkedInstance) {
534 | if (target._jscLinkedInstance.showOnClick) {
535 | target._jscLinkedInstance.show();
536 | }
537 | } else if (target._jscControlName) {
538 | jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse');
539 | } else {
540 | // Mouse is outside the picker controls -> hide the color picker!
541 | if (jsc.picker && jsc.picker.owner) {
542 | jsc.picker.owner.hide();
543 | }
544 | }
545 | },
546 |
547 |
548 | onDocumentTouchStart : function (e) {
549 | if (!e) { e = window.event; }
550 |
551 | var target = e.explicitOriginalTarget || e.path[0];
552 |
553 | if (target._jscLinkedInstance) {
554 | if (target._jscLinkedInstance.showOnClick) {
555 |
556 | target._jscLinkedInstance.show();
557 | }
558 | } else if (target._jscControlName) {
559 | jsc.onControlPointerStart(e, target, target._jscControlName, 'touch');
560 | } else {
561 | if (jsc.picker && jsc.picker.owner) {
562 | jsc.picker.owner.hide();
563 | }
564 | }
565 | },
566 |
567 |
568 | onWindowResize : function (e) {
569 | jsc.redrawPosition();
570 | },
571 |
572 |
573 | onParentScroll : function (e) {
574 | // hide the picker when one of the parent elements is scrolled
575 | if (jsc.picker && jsc.picker.owner) {
576 | jsc.picker.owner.hide();
577 | }
578 | },
579 |
580 |
581 | _pointerMoveEvent : {
582 | mouse: 'mousemove',
583 | touch: 'touchmove'
584 | },
585 | _pointerEndEvent : {
586 | mouse: 'mouseup',
587 | touch: 'touchend'
588 | },
589 |
590 |
591 | _pointerOrigin : null,
592 | _capturedTarget : null,
593 |
594 |
595 | onControlPointerStart : function (e, target, controlName, pointerType) {
596 | var thisObj = target._jscInstance;
597 |
598 | jsc.preventDefault(e);
599 | jsc.captureTarget(target);
600 |
601 | var registerDragEvents = function (doc, offset) {
602 | jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType],
603 | jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset));
604 | jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType],
605 | jsc.onDocumentPointerEnd(e, target, controlName, pointerType));
606 | };
607 |
608 | registerDragEvents(document, [0, 0]);
609 |
610 | if (window.parent && window.frameElement) {
611 | var rect = window.frameElement.getBoundingClientRect();
612 | var ofs = [-rect.left, -rect.top];
613 | registerDragEvents(window.parent.window.document, ofs);
614 | }
615 |
616 | var abs = jsc.getAbsPointerPos(e);
617 | var rel = jsc.getRelPointerPos(e);
618 | jsc._pointerOrigin = {
619 | x: abs.x - rel.x,
620 | y: abs.y - rel.y
621 | };
622 |
623 | switch (controlName) {
624 | case 'pad':
625 | // if the slider is at the bottom, move it up
626 | switch (jsc.getSliderComponent(thisObj)) {
627 | case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break;
628 | case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break;
629 | }
630 | jsc.setPad(thisObj, e, 0, 0);
631 | break;
632 |
633 | case 'sld':
634 | jsc.setSld(thisObj, e, 0);
635 | break;
636 | }
637 |
638 | jsc.dispatchFineChange(thisObj);
639 | },
640 |
641 |
642 | onDocumentPointerMove : function (e, target, controlName, pointerType, offset) {
643 | return function (e) {
644 | var thisObj = target._jscInstance;
645 | switch (controlName) {
646 | case 'pad':
647 | if (!e) { e = window.event; }
648 | jsc.setPad(thisObj, e, offset[0], offset[1]);
649 | jsc.dispatchFineChange(thisObj);
650 | break;
651 |
652 | case 'sld':
653 | if (!e) { e = window.event; }
654 | jsc.setSld(thisObj, e, offset[1]);
655 | jsc.dispatchFineChange(thisObj);
656 | break;
657 | }
658 | }
659 | },
660 |
661 |
662 | onDocumentPointerEnd : function (e, target, controlName, pointerType) {
663 | return function (e) {
664 | var thisObj = target._jscInstance;
665 | jsc.detachGroupEvents('drag');
666 | jsc.releaseTarget();
667 | // Always dispatch changes after detaching outstanding mouse handlers,
668 | // in case some user interaction will occur in user's onchange callback
669 | // that would intrude with current mouse events
670 | jsc.dispatchChange(thisObj);
671 | };
672 | },
673 |
674 |
675 | dispatchChange : function (thisObj) {
676 | if (thisObj.valueElement) {
677 | if (jsc.isElementType(thisObj.valueElement, 'input')) {
678 | jsc.fireEvent(thisObj.valueElement, 'change');
679 | }
680 | }
681 | },
682 |
683 |
684 | dispatchFineChange : function (thisObj) {
685 | if (thisObj.onFineChange) {
686 | var callback;
687 | if (typeof thisObj.onFineChange === 'string') {
688 | callback = new Function (thisObj.onFineChange);
689 | } else {
690 | callback = thisObj.onFineChange;
691 | }
692 | callback.call(thisObj);
693 | }
694 | },
695 |
696 |
697 | setPad : function (thisObj, e, ofsX, ofsY) {
698 | var pointerAbs = jsc.getAbsPointerPos(e);
699 | var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth;
700 | var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth;
701 |
702 | var xVal = x * (360 / (thisObj.width - 1));
703 | var yVal = 100 - (y * (100 / (thisObj.height - 1)));
704 |
705 | switch (jsc.getPadYComponent(thisObj)) {
706 | case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break;
707 | case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break;
708 | }
709 | },
710 |
711 |
712 | setSld : function (thisObj, e, ofsY) {
713 | var pointerAbs = jsc.getAbsPointerPos(e);
714 | var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth;
715 |
716 | var yVal = 100 - (y * (100 / (thisObj.height - 1)));
717 |
718 | switch (jsc.getSliderComponent(thisObj)) {
719 | case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break;
720 | case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break;
721 | }
722 | },
723 |
724 |
725 | _vmlNS : 'jsc_vml_',
726 | _vmlCSS : 'jsc_vml_css_',
727 | _vmlReady : false,
728 |
729 |
730 | initVML : function () {
731 | if (!jsc._vmlReady) {
732 | // init VML namespace
733 | var doc = document;
734 | if (!doc.namespaces[jsc._vmlNS]) {
735 | doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml');
736 | }
737 | if (!doc.styleSheets[jsc._vmlCSS]) {
738 | var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image'];
739 | var ss = doc.createStyleSheet();
740 | ss.owningElement.id = jsc._vmlCSS;
741 | for (var i = 0; i < tags.length; i += 1) {
742 | ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);');
743 | }
744 | }
745 | jsc._vmlReady = true;
746 | }
747 | },
748 |
749 |
750 | createPalette : function () {
751 |
752 | var paletteObj = {
753 | elm: null,
754 | draw: null
755 | };
756 |
757 | if (jsc.isCanvasSupported) {
758 | // Canvas implementation for modern browsers
759 |
760 | var canvas = document.createElement('canvas');
761 | var ctx = canvas.getContext('2d');
762 |
763 | var drawFunc = function (width, height, type) {
764 | canvas.width = width;
765 | canvas.height = height;
766 |
767 | ctx.clearRect(0, 0, canvas.width, canvas.height);
768 |
769 | var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0);
770 | hGrad.addColorStop(0 / 6, '#F00');
771 | hGrad.addColorStop(1 / 6, '#FF0');
772 | hGrad.addColorStop(2 / 6, '#0F0');
773 | hGrad.addColorStop(3 / 6, '#0FF');
774 | hGrad.addColorStop(4 / 6, '#00F');
775 | hGrad.addColorStop(5 / 6, '#F0F');
776 | hGrad.addColorStop(6 / 6, '#F00');
777 |
778 | ctx.fillStyle = hGrad;
779 | ctx.fillRect(0, 0, canvas.width, canvas.height);
780 |
781 | var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
782 | switch (type.toLowerCase()) {
783 | case 's':
784 | vGrad.addColorStop(0, 'rgba(255,255,255,0)');
785 | vGrad.addColorStop(1, 'rgba(255,255,255,1)');
786 | break;
787 | case 'v':
788 | vGrad.addColorStop(0, 'rgba(0,0,0,0)');
789 | vGrad.addColorStop(1, 'rgba(0,0,0,1)');
790 | break;
791 | }
792 | ctx.fillStyle = vGrad;
793 | ctx.fillRect(0, 0, canvas.width, canvas.height);
794 | };
795 |
796 | paletteObj.elm = canvas;
797 | paletteObj.draw = drawFunc;
798 |
799 | } else {
800 | // VML fallback for IE 7 and 8
801 |
802 | jsc.initVML();
803 |
804 | var vmlContainer = document.createElement('div');
805 | vmlContainer.style.position = 'relative';
806 | vmlContainer.style.overflow = 'hidden';
807 |
808 | var hGrad = document.createElement(jsc._vmlNS + ':fill');
809 | hGrad.type = 'gradient';
810 | hGrad.method = 'linear';
811 | hGrad.angle = '90';
812 | hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0'
813 |
814 | var hRect = document.createElement(jsc._vmlNS + ':rect');
815 | hRect.style.position = 'absolute';
816 | hRect.style.left = -1 + 'px';
817 | hRect.style.top = -1 + 'px';
818 | hRect.stroked = false;
819 | hRect.appendChild(hGrad);
820 | vmlContainer.appendChild(hRect);
821 |
822 | var vGrad = document.createElement(jsc._vmlNS + ':fill');
823 | vGrad.type = 'gradient';
824 | vGrad.method = 'linear';
825 | vGrad.angle = '180';
826 | vGrad.opacity = '0';
827 |
828 | var vRect = document.createElement(jsc._vmlNS + ':rect');
829 | vRect.style.position = 'absolute';
830 | vRect.style.left = -1 + 'px';
831 | vRect.style.top = -1 + 'px';
832 | vRect.stroked = false;
833 | vRect.appendChild(vGrad);
834 | vmlContainer.appendChild(vRect);
835 |
836 | var drawFunc = function (width, height, type) {
837 | vmlContainer.style.width = width + 'px';
838 | vmlContainer.style.height = height + 'px';
839 |
840 | hRect.style.width =
841 | vRect.style.width =
842 | (width + 1) + 'px';
843 | hRect.style.height =
844 | vRect.style.height =
845 | (height + 1) + 'px';
846 |
847 | // Colors must be specified during every redraw, otherwise IE won't display
848 | // a full gradient during a subsequential redraw
849 | hGrad.color = '#F00';
850 | hGrad.color2 = '#F00';
851 |
852 | switch (type.toLowerCase()) {
853 | case 's':
854 | vGrad.color = vGrad.color2 = '#FFF';
855 | break;
856 | case 'v':
857 | vGrad.color = vGrad.color2 = '#000';
858 | break;
859 | }
860 | };
861 |
862 | paletteObj.elm = vmlContainer;
863 | paletteObj.draw = drawFunc;
864 | }
865 |
866 | return paletteObj;
867 | },
868 |
869 |
870 | createSliderGradient : function () {
871 |
872 | var sliderObj = {
873 | elm: null,
874 | draw: null
875 | };
876 |
877 | if (jsc.isCanvasSupported) {
878 | // Canvas implementation for modern browsers
879 |
880 | var canvas = document.createElement('canvas');
881 | var ctx = canvas.getContext('2d');
882 |
883 | var drawFunc = function (width, height, color1, color2) {
884 | canvas.width = width;
885 | canvas.height = height;
886 |
887 | ctx.clearRect(0, 0, canvas.width, canvas.height);
888 |
889 | var grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
890 | grad.addColorStop(0, color1);
891 | grad.addColorStop(1, color2);
892 |
893 | ctx.fillStyle = grad;
894 | ctx.fillRect(0, 0, canvas.width, canvas.height);
895 | };
896 |
897 | sliderObj.elm = canvas;
898 | sliderObj.draw = drawFunc;
899 |
900 | } else {
901 | // VML fallback for IE 7 and 8
902 |
903 | jsc.initVML();
904 |
905 | var vmlContainer = document.createElement('div');
906 | vmlContainer.style.position = 'relative';
907 | vmlContainer.style.overflow = 'hidden';
908 |
909 | var grad = document.createElement(jsc._vmlNS + ':fill');
910 | grad.type = 'gradient';
911 | grad.method = 'linear';
912 | grad.angle = '180';
913 |
914 | var rect = document.createElement(jsc._vmlNS + ':rect');
915 | rect.style.position = 'absolute';
916 | rect.style.left = -1 + 'px';
917 | rect.style.top = -1 + 'px';
918 | rect.stroked = false;
919 | rect.appendChild(grad);
920 | vmlContainer.appendChild(rect);
921 |
922 | var drawFunc = function (width, height, color1, color2) {
923 | vmlContainer.style.width = width + 'px';
924 | vmlContainer.style.height = height + 'px';
925 |
926 | rect.style.width = (width + 1) + 'px';
927 | rect.style.height = (height + 1) + 'px';
928 |
929 | grad.color = color1;
930 | grad.color2 = color2;
931 | };
932 |
933 | sliderObj.elm = vmlContainer;
934 | sliderObj.draw = drawFunc;
935 | }
936 |
937 | return sliderObj;
938 | },
939 |
940 |
941 | leaveValue : 1<<0,
942 | leaveStyle : 1<<1,
943 | leavePad : 1<<2,
944 | leaveSld : 1<<3,
945 |
946 |
947 | BoxShadow : (function () {
948 | var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) {
949 | this.hShadow = hShadow;
950 | this.vShadow = vShadow;
951 | this.blur = blur;
952 | this.spread = spread;
953 | this.color = color;
954 | this.inset = !!inset;
955 | };
956 |
957 | BoxShadow.prototype.toString = function () {
958 | var vals = [
959 | Math.round(this.hShadow) + 'px',
960 | Math.round(this.vShadow) + 'px',
961 | Math.round(this.blur) + 'px',
962 | Math.round(this.spread) + 'px',
963 | this.color
964 | ];
965 | if (this.inset) {
966 | vals.push('inset');
967 | }
968 | return vals.join(' ');
969 | };
970 |
971 | return BoxShadow;
972 | })(),
973 |
974 |
975 | //
976 | // Usage:
977 | // var myColor = new jscolor( [, ])
978 | //
979 |
980 | jscolor : function (targetElement, options) {
981 |
982 | // General options
983 | //
984 | this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB()
985 | this.valueElement = targetElement; // element that will be used to display and input the color code
986 | this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor
987 | this.required = true; // whether the associated text can be left empty
988 | this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace)
989 | this.hash = false; // whether to prefix the HEX color code with # symbol
990 | this.uppercase = true; // whether to show the color code in upper case
991 | this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code)
992 | this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it
993 | this.overwriteImportant = false; // whether to overwrite colors of styleElement using !important
994 | this.minS = 0; // min allowed saturation (0 - 100)
995 | this.maxS = 100; // max allowed saturation (0 - 100)
996 | this.minV = 0; // min allowed value (brightness) (0 - 100)
997 | this.maxV = 100; // max allowed value (brightness) (0 - 100)
998 |
999 | // Accessing the picked color
1000 | //
1001 | this.hsv = [0, 0, 100]; // read-only [0-360, 0-100, 0-100]
1002 | this.rgb = [255, 255, 255]; // read-only [0-255, 0-255, 0-255]
1003 |
1004 | // Color Picker options
1005 | //
1006 | this.width = 181; // width of color palette (in px)
1007 | this.height = 101; // height of color palette (in px)
1008 | this.showOnClick = true; // whether to display the color picker when user clicks on its target element
1009 | this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls
1010 | this.position = 'bottom'; // left | right | top | bottom - position relative to the target element
1011 | this.smartPosition = true; // automatically change picker position when there is not enough space for it
1012 | this.sliderSize = 16; // px
1013 | this.crossSize = 8; // px
1014 | this.closable = false; // whether to display the Close button
1015 | this.closeText = 'Close';
1016 | this.buttonColor = '#000000'; // CSS color
1017 | this.buttonHeight = 18; // px
1018 | this.padding = 12; // px
1019 | this.backgroundColor = '#FFFFFF'; // CSS color
1020 | this.borderWidth = 1; // px
1021 | this.borderColor = '#BBBBBB'; // CSS color
1022 | this.borderRadius = 8; // px
1023 | this.insetWidth = 1; // px
1024 | this.insetColor = '#BBBBBB'; // CSS color
1025 | this.shadow = true; // whether to display shadow
1026 | this.shadowBlur = 15; // px
1027 | this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color
1028 | this.pointerColor = '#4C4C4C'; // px
1029 | this.pointerBorderColor = '#FFFFFF'; // px
1030 | this.pointerBorderWidth = 1; // px
1031 | this.pointerThickness = 2; // px
1032 | this.zIndex = 1000;
1033 | this.container = null; // where to append the color picker (BODY element by default)
1034 |
1035 |
1036 | for (var opt in options) {
1037 | if (options.hasOwnProperty(opt)) {
1038 | this[opt] = options[opt];
1039 | }
1040 | }
1041 |
1042 |
1043 | this.hide = function () {
1044 | if (isPickerOwner()) {
1045 | detachPicker();
1046 | }
1047 | };
1048 |
1049 |
1050 | this.show = function () {
1051 | drawPicker();
1052 | };
1053 |
1054 |
1055 | this.redraw = function () {
1056 | if (isPickerOwner()) {
1057 | drawPicker();
1058 | }
1059 | };
1060 |
1061 |
1062 | this.importColor = function () {
1063 | if (!this.valueElement) {
1064 | this.exportColor();
1065 | } else {
1066 | if (jsc.isElementType(this.valueElement, 'input')) {
1067 | if (!this.refine) {
1068 | if (!this.fromString(this.valueElement.value, jsc.leaveValue)) {
1069 | if (this.styleElement) {
1070 | this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage;
1071 | this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor;
1072 | this.styleElement.style.color = this.styleElement._jscOrigStyle.color;
1073 | }
1074 | this.exportColor(jsc.leaveValue | jsc.leaveStyle);
1075 | }
1076 | } else if (!this.required && /^\s*$/.test(this.valueElement.value)) {
1077 | this.valueElement.value = '';
1078 | if (this.styleElement) {
1079 | this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage;
1080 | this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor;
1081 | this.styleElement.style.color = this.styleElement._jscOrigStyle.color;
1082 | }
1083 | this.exportColor(jsc.leaveValue | jsc.leaveStyle);
1084 |
1085 | } else if (this.fromString(this.valueElement.value)) {
1086 | // managed to import color successfully from the value -> OK, don't do anything
1087 | } else {
1088 | this.exportColor();
1089 | }
1090 | } else {
1091 | // not an input element -> doesn't have any value
1092 | this.exportColor();
1093 | }
1094 | }
1095 | };
1096 |
1097 |
1098 | this.exportColor = function (flags) {
1099 | if (!(flags & jsc.leaveValue) && this.valueElement) {
1100 | var value = this.toString();
1101 | if (this.uppercase) { value = value.toUpperCase(); }
1102 | if (this.hash) { value = '#' + value; }
1103 |
1104 | if (jsc.isElementType(this.valueElement, 'input')) {
1105 | this.valueElement.value = value;
1106 | } else {
1107 | this.valueElement.innerHTML = value;
1108 | }
1109 | }
1110 | if (!(flags & jsc.leaveStyle)) {
1111 | if (this.styleElement) {
1112 | var bgColor = '#' + this.toString();
1113 | var fgColor = this.isLight() ? '#000' : '#FFF';
1114 |
1115 | this.styleElement.style.backgroundImage = 'none';
1116 | this.styleElement.style.backgroundColor = bgColor;
1117 | this.styleElement.style.color = fgColor;
1118 |
1119 | if (this.overwriteImportant) {
1120 | this.styleElement.setAttribute('style',
1121 | 'background: ' + bgColor + ' !important; ' +
1122 | 'color: ' + fgColor + ' !important;'
1123 | );
1124 | }
1125 | }
1126 | }
1127 | if (!(flags & jsc.leavePad) && isPickerOwner()) {
1128 | redrawPad();
1129 | }
1130 | if (!(flags & jsc.leaveSld) && isPickerOwner()) {
1131 | redrawSld();
1132 | }
1133 | };
1134 |
1135 |
1136 | // h: 0-360
1137 | // s: 0-100
1138 | // v: 0-100
1139 | //
1140 | this.fromHSV = function (h, s, v, flags) { // null = don't change
1141 | if (h !== null) {
1142 | if (isNaN(h)) { return false; }
1143 | h = Math.max(0, Math.min(360, h));
1144 | }
1145 | if (s !== null) {
1146 | if (isNaN(s)) { return false; }
1147 | s = Math.max(0, Math.min(100, this.maxS, s), this.minS);
1148 | }
1149 | if (v !== null) {
1150 | if (isNaN(v)) { return false; }
1151 | v = Math.max(0, Math.min(100, this.maxV, v), this.minV);
1152 | }
1153 |
1154 | this.rgb = HSV_RGB(
1155 | h===null ? this.hsv[0] : (this.hsv[0]=h),
1156 | s===null ? this.hsv[1] : (this.hsv[1]=s),
1157 | v===null ? this.hsv[2] : (this.hsv[2]=v)
1158 | );
1159 |
1160 | this.exportColor(flags);
1161 | };
1162 |
1163 |
1164 | // r: 0-255
1165 | // g: 0-255
1166 | // b: 0-255
1167 | //
1168 | this.fromRGB = function (r, g, b, flags) { // null = don't change
1169 | if (r !== null) {
1170 | if (isNaN(r)) { return false; }
1171 | r = Math.max(0, Math.min(255, r));
1172 | }
1173 | if (g !== null) {
1174 | if (isNaN(g)) { return false; }
1175 | g = Math.max(0, Math.min(255, g));
1176 | }
1177 | if (b !== null) {
1178 | if (isNaN(b)) { return false; }
1179 | b = Math.max(0, Math.min(255, b));
1180 | }
1181 |
1182 | var hsv = RGB_HSV(
1183 | r===null ? this.rgb[0] : r,
1184 | g===null ? this.rgb[1] : g,
1185 | b===null ? this.rgb[2] : b
1186 | );
1187 | if (hsv[0] !== null) {
1188 | this.hsv[0] = Math.max(0, Math.min(360, hsv[0]));
1189 | }
1190 | if (hsv[2] !== 0) {
1191 | this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1]));
1192 | }
1193 | this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2]));
1194 |
1195 | // update RGB according to final HSV, as some values might be trimmed
1196 | var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]);
1197 | this.rgb[0] = rgb[0];
1198 | this.rgb[1] = rgb[1];
1199 | this.rgb[2] = rgb[2];
1200 |
1201 | this.exportColor(flags);
1202 | };
1203 |
1204 |
1205 | this.fromString = function (str, flags) {
1206 | var m;
1207 | if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) {
1208 | // HEX notation
1209 | //
1210 |
1211 | if (m[1].length === 6) {
1212 | // 6-char notation
1213 | this.fromRGB(
1214 | parseInt(m[1].substr(0,2),16),
1215 | parseInt(m[1].substr(2,2),16),
1216 | parseInt(m[1].substr(4,2),16),
1217 | flags
1218 | );
1219 | } else {
1220 | // 3-char notation
1221 | this.fromRGB(
1222 | parseInt(m[1].charAt(0) + m[1].charAt(0),16),
1223 | parseInt(m[1].charAt(1) + m[1].charAt(1),16),
1224 | parseInt(m[1].charAt(2) + m[1].charAt(2),16),
1225 | flags
1226 | );
1227 | }
1228 | return true;
1229 |
1230 | } else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) {
1231 | var params = m[1].split(',');
1232 | var re = /^\s*(\d*)(\.\d+)?\s*$/;
1233 | var mR, mG, mB;
1234 | if (
1235 | params.length >= 3 &&
1236 | (mR = params[0].match(re)) &&
1237 | (mG = params[1].match(re)) &&
1238 | (mB = params[2].match(re))
1239 | ) {
1240 | var r = parseFloat((mR[1] || '0') + (mR[2] || ''));
1241 | var g = parseFloat((mG[1] || '0') + (mG[2] || ''));
1242 | var b = parseFloat((mB[1] || '0') + (mB[2] || ''));
1243 | this.fromRGB(r, g, b, flags);
1244 | return true;
1245 | }
1246 | }
1247 | return false;
1248 | };
1249 |
1250 |
1251 | this.toString = function () {
1252 | return (
1253 | (0x100 | Math.round(this.rgb[0])).toString(16).substr(1) +
1254 | (0x100 | Math.round(this.rgb[1])).toString(16).substr(1) +
1255 | (0x100 | Math.round(this.rgb[2])).toString(16).substr(1)
1256 | );
1257 | };
1258 |
1259 |
1260 | this.toHEXString = function () {
1261 | return '#' + this.toString().toUpperCase();
1262 | };
1263 |
1264 |
1265 | this.toRGBString = function () {
1266 | return ('rgb(' +
1267 | Math.round(this.rgb[0]) + ',' +
1268 | Math.round(this.rgb[1]) + ',' +
1269 | Math.round(this.rgb[2]) + ')'
1270 | );
1271 | };
1272 |
1273 |
1274 | this.isLight = function () {
1275 | return (
1276 | 0.213 * this.rgb[0] +
1277 | 0.715 * this.rgb[1] +
1278 | 0.072 * this.rgb[2] >
1279 | 255 / 2
1280 | );
1281 | };
1282 |
1283 |
1284 | this._processParentElementsInDOM = function () {
1285 | if (this._linkedElementsProcessed) { return; }
1286 | this._linkedElementsProcessed = true;
1287 |
1288 | var elm = this.targetElement;
1289 | do {
1290 | // If the target element or one of its parent nodes has fixed position,
1291 | // then use fixed positioning instead
1292 | //
1293 | // Note: In Firefox, getComputedStyle returns null in a hidden iframe,
1294 | // that's why we need to check if the returned style object is non-empty
1295 | /* var currStyle = jsc.getStyle(elm);
1296 | if (currStyle && currStyle.position.toLowerCase() === 'fixed') {
1297 | this.fixed = true;
1298 | }*/
1299 |
1300 | if (elm !== this.targetElement) {
1301 | // Ensure to attach onParentScroll only once to each parent element
1302 | // (multiple targetElements can share the same parent nodes)
1303 | //
1304 | // Note: It's not just offsetParents that can be scrollable,
1305 | // that's why we loop through all parent nodes
1306 | if (!elm._jscEventsAttached) {
1307 | jsc.attachEvent(elm, 'scroll', jsc.onParentScroll);
1308 | elm._jscEventsAttached = true;
1309 | }
1310 | }
1311 | } while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body'));
1312 | };
1313 |
1314 |
1315 | // r: 0-255
1316 | // g: 0-255
1317 | // b: 0-255
1318 | //
1319 | // returns: [ 0-360, 0-100, 0-100 ]
1320 | //
1321 | function RGB_HSV (r, g, b) {
1322 | r /= 255;
1323 | g /= 255;
1324 | b /= 255;
1325 | var n = Math.min(Math.min(r,g),b);
1326 | var v = Math.max(Math.max(r,g),b);
1327 | var m = v - n;
1328 | if (m === 0) { return [ null, 0, 100 * v ]; }
1329 | var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m);
1330 | return [
1331 | 60 * (h===6?0:h),
1332 | 100 * (m/v),
1333 | 100 * v
1334 | ];
1335 | }
1336 |
1337 |
1338 | // h: 0-360
1339 | // s: 0-100
1340 | // v: 0-100
1341 | //
1342 | // returns: [ 0-255, 0-255, 0-255 ]
1343 | //
1344 | function HSV_RGB (h, s, v) {
1345 | var u = 255 * (v / 100);
1346 |
1347 | if (h === null) {
1348 | return [ u, u, u ];
1349 | }
1350 |
1351 | h /= 60;
1352 | s /= 100;
1353 |
1354 | var i = Math.floor(h);
1355 | var f = i%2 ? h-i : 1-(h-i);
1356 | var m = u * (1 - s);
1357 | var n = u * (1 - s * f);
1358 | switch (i) {
1359 | case 6:
1360 | case 0: return [u,n,m];
1361 | case 1: return [n,u,m];
1362 | case 2: return [m,u,n];
1363 | case 3: return [m,n,u];
1364 | case 4: return [n,m,u];
1365 | case 5: return [u,m,n];
1366 | }
1367 | }
1368 |
1369 |
1370 | function detachPicker () {
1371 | jsc.unsetClass(THIS.targetElement, THIS.activeClass);
1372 | jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap);
1373 | delete jsc.picker.owner;
1374 | }
1375 |
1376 |
1377 | function drawPicker () {
1378 |
1379 | // At this point, when drawing the picker, we know what the parent elements are
1380 | // and we can do all related DOM operations, such as registering events on them
1381 | // or checking their positioning
1382 | THIS._processParentElementsInDOM();
1383 |
1384 | if (!jsc.picker) {
1385 | jsc.picker = {
1386 | owner: null,
1387 | wrap : document.createElement('div'),
1388 | box : document.createElement('div'),
1389 | boxS : document.createElement('div'), // shadow area
1390 | boxB : document.createElement('div'), // border
1391 | pad : document.createElement('div'),
1392 | padB : document.createElement('div'), // border
1393 | padM : document.createElement('div'), // mouse/touch area
1394 | padPal : jsc.createPalette(),
1395 | cross : document.createElement('div'),
1396 | crossBY : document.createElement('div'), // border Y
1397 | crossBX : document.createElement('div'), // border X
1398 | crossLY : document.createElement('div'), // line Y
1399 | crossLX : document.createElement('div'), // line X
1400 | sld : document.createElement('div'),
1401 | sldB : document.createElement('div'), // border
1402 | sldM : document.createElement('div'), // mouse/touch area
1403 | sldGrad : jsc.createSliderGradient(),
1404 | sldPtrS : document.createElement('div'), // slider pointer spacer
1405 | sldPtrIB : document.createElement('div'), // slider pointer inner border
1406 | sldPtrMB : document.createElement('div'), // slider pointer middle border
1407 | sldPtrOB : document.createElement('div'), // slider pointer outer border
1408 | btn : document.createElement('div'),
1409 | btnT : document.createElement('span') // text
1410 | };
1411 |
1412 | jsc.picker.pad.appendChild(jsc.picker.padPal.elm);
1413 | jsc.picker.padB.appendChild(jsc.picker.pad);
1414 | jsc.picker.cross.appendChild(jsc.picker.crossBY);
1415 | jsc.picker.cross.appendChild(jsc.picker.crossBX);
1416 | jsc.picker.cross.appendChild(jsc.picker.crossLY);
1417 | jsc.picker.cross.appendChild(jsc.picker.crossLX);
1418 | jsc.picker.padB.appendChild(jsc.picker.cross);
1419 | jsc.picker.box.appendChild(jsc.picker.padB);
1420 | jsc.picker.box.appendChild(jsc.picker.padM);
1421 |
1422 | jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm);
1423 | jsc.picker.sldB.appendChild(jsc.picker.sld);
1424 | jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB);
1425 | jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB);
1426 | jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB);
1427 | jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS);
1428 | jsc.picker.box.appendChild(jsc.picker.sldB);
1429 | jsc.picker.box.appendChild(jsc.picker.sldM);
1430 |
1431 | jsc.picker.btn.appendChild(jsc.picker.btnT);
1432 | jsc.picker.box.appendChild(jsc.picker.btn);
1433 |
1434 | jsc.picker.boxB.appendChild(jsc.picker.box);
1435 | jsc.picker.wrap.appendChild(jsc.picker.boxS);
1436 | jsc.picker.wrap.appendChild(jsc.picker.boxB);
1437 | }
1438 |
1439 | var p = jsc.picker;
1440 |
1441 | var displaySlider = !!jsc.getSliderComponent(THIS);
1442 | var dims = jsc.getPickerDims(THIS);
1443 | var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize);
1444 | var padToSliderPadding = jsc.getPadToSliderPadding(THIS);
1445 | var borderRadius = Math.min(
1446 | THIS.borderRadius,
1447 | Math.round(THIS.padding * Math.PI)); // px
1448 | var padCursor = 'crosshair';
1449 |
1450 | // wrap
1451 | p.wrap.style.clear = 'both';
1452 | p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px';
1453 | p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px';
1454 | p.wrap.style.zIndex = THIS.zIndex;
1455 |
1456 | // picker
1457 | p.box.style.width = dims[0] + 'px';
1458 | p.box.style.height = dims[1] + 'px';
1459 |
1460 | p.boxS.style.position = 'absolute';
1461 | p.boxS.style.left = '0';
1462 | p.boxS.style.top = '0';
1463 | p.boxS.style.width = '100%';
1464 | p.boxS.style.height = '100%';
1465 | jsc.setBorderRadius(p.boxS, borderRadius + 'px');
1466 |
1467 | // picker border
1468 | p.boxB.style.position = 'relative';
1469 | p.boxB.style.border = THIS.borderWidth + 'px solid';
1470 | p.boxB.style.borderColor = THIS.borderColor;
1471 | p.boxB.style.background = THIS.backgroundColor;
1472 | jsc.setBorderRadius(p.boxB, borderRadius + 'px');
1473 |
1474 | // IE hack:
1475 | // If the element is transparent, IE will trigger the event on the elements under it,
1476 | // e.g. on Canvas or on elements with border
1477 | p.padM.style.background =
1478 | p.sldM.style.background =
1479 | '#FFF';
1480 | jsc.setStyle(p.padM, 'opacity', '0');
1481 | jsc.setStyle(p.sldM, 'opacity', '0');
1482 |
1483 | // pad
1484 | p.pad.style.position = 'relative';
1485 | p.pad.style.width = THIS.width + 'px';
1486 | p.pad.style.height = THIS.height + 'px';
1487 |
1488 | // pad palettes (HSV and HVS)
1489 | p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS));
1490 |
1491 | // pad border
1492 | p.padB.style.position = 'absolute';
1493 | p.padB.style.left = THIS.padding + 'px';
1494 | p.padB.style.top = THIS.padding + 'px';
1495 | p.padB.style.border = THIS.insetWidth + 'px solid';
1496 | p.padB.style.borderColor = THIS.insetColor;
1497 |
1498 | // pad mouse area
1499 | p.padM._jscInstance = THIS;
1500 | p.padM._jscControlName = 'pad';
1501 | p.padM.style.position = 'absolute';
1502 | p.padM.style.left = '0';
1503 | p.padM.style.top = '0';
1504 | p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px';
1505 | p.padM.style.height = dims[1] + 'px';
1506 | p.padM.style.cursor = padCursor;
1507 |
1508 | // pad cross
1509 | p.cross.style.position = 'absolute';
1510 | p.cross.style.left =
1511 | p.cross.style.top =
1512 | '0';
1513 | p.cross.style.width =
1514 | p.cross.style.height =
1515 | crossOuterSize + 'px';
1516 |
1517 | // pad cross border Y and X
1518 | p.crossBY.style.position =
1519 | p.crossBX.style.position =
1520 | 'absolute';
1521 | p.crossBY.style.background =
1522 | p.crossBX.style.background =
1523 | THIS.pointerBorderColor;
1524 | p.crossBY.style.width =
1525 | p.crossBX.style.height =
1526 | (2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px';
1527 | p.crossBY.style.height =
1528 | p.crossBX.style.width =
1529 | crossOuterSize + 'px';
1530 | p.crossBY.style.left =
1531 | p.crossBX.style.top =
1532 | (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px';
1533 | p.crossBY.style.top =
1534 | p.crossBX.style.left =
1535 | '0';
1536 |
1537 | // pad cross line Y and X
1538 | p.crossLY.style.position =
1539 | p.crossLX.style.position =
1540 | 'absolute';
1541 | p.crossLY.style.background =
1542 | p.crossLX.style.background =
1543 | THIS.pointerColor;
1544 | p.crossLY.style.height =
1545 | p.crossLX.style.width =
1546 | (crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px';
1547 | p.crossLY.style.width =
1548 | p.crossLX.style.height =
1549 | THIS.pointerThickness + 'px';
1550 | p.crossLY.style.left =
1551 | p.crossLX.style.top =
1552 | (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px';
1553 | p.crossLY.style.top =
1554 | p.crossLX.style.left =
1555 | THIS.pointerBorderWidth + 'px';
1556 |
1557 | // slider
1558 | p.sld.style.overflow = 'hidden';
1559 | p.sld.style.width = THIS.sliderSize + 'px';
1560 | p.sld.style.height = THIS.height + 'px';
1561 |
1562 | // slider gradient
1563 | p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000');
1564 |
1565 | // slider border
1566 | p.sldB.style.display = displaySlider ? 'block' : 'none';
1567 | p.sldB.style.position = 'absolute';
1568 | p.sldB.style.right = THIS.padding + 'px';
1569 | p.sldB.style.top = THIS.padding + 'px';
1570 | p.sldB.style.border = THIS.insetWidth + 'px solid';
1571 | p.sldB.style.borderColor = THIS.insetColor;
1572 |
1573 | // slider mouse area
1574 | p.sldM._jscInstance = THIS;
1575 | p.sldM._jscControlName = 'sld';
1576 | p.sldM.style.display = displaySlider ? 'block' : 'none';
1577 | p.sldM.style.position = 'absolute';
1578 | p.sldM.style.right = '0';
1579 | p.sldM.style.top = '0';
1580 | p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px';
1581 | p.sldM.style.height = dims[1] + 'px';
1582 | p.sldM.style.cursor = 'default';
1583 |
1584 | // slider pointer inner and outer border
1585 | p.sldPtrIB.style.border =
1586 | p.sldPtrOB.style.border =
1587 | THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor;
1588 |
1589 | // slider pointer outer border
1590 | p.sldPtrOB.style.position = 'absolute';
1591 | p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px';
1592 | p.sldPtrOB.style.top = '0';
1593 |
1594 | // slider pointer middle border
1595 | p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor;
1596 |
1597 | // slider pointer spacer
1598 | p.sldPtrS.style.width = THIS.sliderSize + 'px';
1599 | p.sldPtrS.style.height = sliderPtrSpace + 'px';
1600 |
1601 | // the Close button
1602 | function setBtnBorder () {
1603 | var insetColors = THIS.insetColor.split(/\s+/);
1604 | var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1];
1605 | p.btn.style.borderColor = outsetColor;
1606 | }
1607 | p.btn.style.display = THIS.closable ? 'block' : 'none';
1608 | p.btn.style.position = 'absolute';
1609 | p.btn.style.left = THIS.padding + 'px';
1610 | p.btn.style.bottom = THIS.padding + 'px';
1611 | p.btn.style.padding = '0 15px';
1612 | p.btn.style.height = THIS.buttonHeight + 'px';
1613 | p.btn.style.border = THIS.insetWidth + 'px solid';
1614 | setBtnBorder();
1615 | p.btn.style.color = THIS.buttonColor;
1616 | p.btn.style.font = '12px sans-serif';
1617 | p.btn.style.textAlign = 'center';
1618 | try {
1619 | p.btn.style.cursor = 'pointer';
1620 | } catch(eOldIE) {
1621 | p.btn.style.cursor = 'hand';
1622 | }
1623 | p.btn.onmousedown = function () {
1624 | THIS.hide();
1625 | };
1626 | p.btnT.style.lineHeight = THIS.buttonHeight + 'px';
1627 | p.btnT.innerHTML = '';
1628 | p.btnT.appendChild(document.createTextNode(THIS.closeText));
1629 |
1630 | // place pointers
1631 | redrawPad();
1632 | redrawSld();
1633 |
1634 | // If we are changing the owner without first closing the picker,
1635 | // make sure to first deal with the old owner
1636 | if (jsc.picker.owner && jsc.picker.owner !== THIS) {
1637 | jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass);
1638 | }
1639 |
1640 | // Set the new picker owner
1641 | jsc.picker.owner = THIS;
1642 |
1643 | // The redrawPosition() method needs picker.owner to be set, that's why we call it here,
1644 | // after setting the owner
1645 | if (jsc.isElementType(container, 'body')) {
1646 | jsc.redrawPosition();
1647 | } else {
1648 | jsc._drawPosition(THIS, 0, 0, 'relative', false);
1649 | }
1650 |
1651 | if (p.wrap.parentNode != container) {
1652 | container.appendChild(p.wrap);
1653 | }
1654 |
1655 | jsc.setClass(THIS.targetElement, THIS.activeClass);
1656 | }
1657 |
1658 |
1659 | function redrawPad () {
1660 | // redraw the pad pointer
1661 | switch (jsc.getPadYComponent(THIS)) {
1662 | case 's': var yComponent = 1; break;
1663 | case 'v': var yComponent = 2; break;
1664 | }
1665 | var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1));
1666 | var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1));
1667 | var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize);
1668 | var ofs = -Math.floor(crossOuterSize / 2);
1669 | jsc.picker.cross.style.left = (x + ofs) + 'px';
1670 | jsc.picker.cross.style.top = (y + ofs) + 'px';
1671 |
1672 | // redraw the slider
1673 | switch (jsc.getSliderComponent(THIS)) {
1674 | case 's':
1675 | var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]);
1676 | var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]);
1677 | var color1 = 'rgb(' +
1678 | Math.round(rgb1[0]) + ',' +
1679 | Math.round(rgb1[1]) + ',' +
1680 | Math.round(rgb1[2]) + ')';
1681 | var color2 = 'rgb(' +
1682 | Math.round(rgb2[0]) + ',' +
1683 | Math.round(rgb2[1]) + ',' +
1684 | Math.round(rgb2[2]) + ')';
1685 | jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2);
1686 | break;
1687 | case 'v':
1688 | var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100);
1689 | var color1 = 'rgb(' +
1690 | Math.round(rgb[0]) + ',' +
1691 | Math.round(rgb[1]) + ',' +
1692 | Math.round(rgb[2]) + ')';
1693 | var color2 = '#000';
1694 | jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2);
1695 | break;
1696 | }
1697 | }
1698 |
1699 |
1700 | function redrawSld () {
1701 | var sldComponent = jsc.getSliderComponent(THIS);
1702 | if (sldComponent) {
1703 | // redraw the slider pointer
1704 | switch (sldComponent) {
1705 | case 's': var yComponent = 1; break;
1706 | case 'v': var yComponent = 2; break;
1707 | }
1708 | var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1));
1709 | jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px';
1710 | }
1711 | }
1712 |
1713 |
1714 | function isPickerOwner () {
1715 | return jsc.picker && jsc.picker.owner === THIS;
1716 | }
1717 |
1718 |
1719 | function blurValue () {
1720 | THIS.importColor();
1721 | }
1722 |
1723 |
1724 | // Find the target element
1725 | if (typeof targetElement === 'string') {
1726 | var id = targetElement;
1727 | var elm = document.getElementById(id);
1728 | if (elm) {
1729 | this.targetElement = elm;
1730 | } else {
1731 | jsc.warn('Could not find target element with ID \'' + id + '\'');
1732 | }
1733 | } else if (targetElement) {
1734 | this.targetElement = targetElement;
1735 | } else {
1736 | jsc.warn('Invalid target element: \'' + targetElement + '\'');
1737 | }
1738 |
1739 | if (this.targetElement._jscLinkedInstance) {
1740 | jsc.warn('Cannot link jscolor twice to the same element. Skipping.');
1741 | return;
1742 | }
1743 | this.targetElement._jscLinkedInstance = this;
1744 |
1745 | // Find the value element
1746 | this.valueElement = jsc.fetchElement(this.valueElement);
1747 | // Find the style element
1748 | this.styleElement = jsc.fetchElement(this.styleElement);
1749 |
1750 | var THIS = this;
1751 | var container =
1752 | this.container ?
1753 | jsc.fetchElement(this.container) :
1754 | document.getElementsByTagName('body')[0];
1755 | var sliderPtrSpace = 3; // px
1756 |
1757 | // For BUTTON elements it's important to stop them from sending the form when clicked
1758 | // (e.g. in Safari)
1759 | if (jsc.isElementType(this.targetElement, 'button')) {
1760 | if (this.targetElement.onclick) {
1761 | var origCallback = this.targetElement.onclick;
1762 | this.targetElement.onclick = function (evt) {
1763 | origCallback.call(this, evt);
1764 | return false;
1765 | };
1766 | } else {
1767 | this.targetElement.onclick = function () { return false; };
1768 | }
1769 | }
1770 |
1771 | /*
1772 | var elm = this.targetElement;
1773 | do {
1774 | // If the target element or one of its offsetParents has fixed position,
1775 | // then use fixed positioning instead
1776 | //
1777 | // Note: In Firefox, getComputedStyle returns null in a hidden iframe,
1778 | // that's why we need to check if the returned style object is non-empty
1779 | var currStyle = jsc.getStyle(elm);
1780 | if (currStyle && currStyle.position.toLowerCase() === 'fixed') {
1781 | this.fixed = true;
1782 | }
1783 |
1784 | if (elm !== this.targetElement) {
1785 | // attach onParentScroll so that we can recompute the picker position
1786 | // when one of the offsetParents is scrolled
1787 | if (!elm._jscEventsAttached) {
1788 | jsc.attachEvent(elm, 'scroll', jsc.onParentScroll);
1789 | elm._jscEventsAttached = true;
1790 | }
1791 | }
1792 | } while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body'));
1793 | */
1794 |
1795 | // valueElement
1796 | if (this.valueElement) {
1797 | if (jsc.isElementType(this.valueElement, 'input')) {
1798 | var updateField = function () {
1799 | THIS.fromString(THIS.valueElement.value, jsc.leaveValue);
1800 | jsc.dispatchFineChange(THIS);
1801 | };
1802 | jsc.attachEvent(this.valueElement, 'keyup', updateField);
1803 | jsc.attachEvent(this.valueElement, 'input', updateField);
1804 | jsc.attachEvent(this.valueElement, 'blur', blurValue);
1805 | this.valueElement.setAttribute('autocomplete', 'off');
1806 | }
1807 | }
1808 |
1809 | // styleElement
1810 | if (this.styleElement) {
1811 | this.styleElement._jscOrigStyle = {
1812 | backgroundImage : this.styleElement.style.backgroundImage,
1813 | backgroundColor : this.styleElement.style.backgroundColor,
1814 | color : this.styleElement.style.color
1815 | };
1816 | }
1817 |
1818 | if (this.value) {
1819 | // Try to set the color from the .value option and if unsuccessful,
1820 | // export the current color
1821 | this.fromString(this.value) || this.exportColor();
1822 | } else {
1823 | this.importColor();
1824 | }
1825 | }
1826 |
1827 | };
1828 |
1829 |
1830 | //================================
1831 | // Public properties and methods
1832 | //================================
1833 |
1834 |
1835 | // By default, search for all elements with class="jscolor" and install a color picker on them.
1836 | //
1837 | // You can change what class name will be looked for by setting the property jscolor.lookupClass
1838 | // anywhere in your HTML document. To completely disable the automatic lookup, set it to null.
1839 | //
1840 | jsc.jscolor.lookupClass = 'jscolor';
1841 |
1842 |
1843 | jsc.jscolor.installByClassName = function (className) {
1844 | var inputElms = document.getElementsByTagName('input');
1845 | var buttonElms = document.getElementsByTagName('button');
1846 |
1847 | jsc.tryInstallOnElements(inputElms, className);
1848 | jsc.tryInstallOnElements(buttonElms, className);
1849 | };
1850 |
1851 |
1852 | jsc.register();
1853 |
1854 |
1855 | return jsc.jscolor;
1856 |
1857 |
1858 | })(); }
1859 |
--------------------------------------------------------------------------------
/www/lib/sha256.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A JavaScript implementation of the SHA256 hash function.
3 | *
4 | * FILE: sha256.js
5 | * VERSION: 0.8
6 | * AUTHOR: Christoph Bichlmeier
7 | *
8 | * NOTE: This version is not tested thoroughly!
9 | *
10 | * Copyright (c) 2003, Christoph Bichlmeier
11 | * All rights reserved.
12 | *
13 | * Redistribution and use in source and binary forms, with or without
14 | * modification, are permitted provided that the following conditions
15 | * are met:
16 | * 1. Redistributions of source code must retain the above copyright
17 | * notice, this list of conditions and the following disclaimer.
18 | * 2. Redistributions in binary form must reproduce the above copyright
19 | * notice, this list of conditions and the following disclaimer in the
20 | * documentation and/or other materials provided with the distribution.
21 | * 3. Neither the name of the copyright holder nor the names of contributors
22 | * may be used to endorse or promote products derived from this software
23 | * without specific prior written permission.
24 | *
25 | * ======================================================================
26 | *
27 | * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY EXPRESS
28 | * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
29 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 | * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
31 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
34 | * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
35 | * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
36 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
37 | * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 | */
39 |
40 | /* SHA256 logical functions */
41 | function rotateRight(n,x) {
42 | return ((x >>> n) | (x << (32 - n)));
43 | }
44 | function choice(x,y,z) {
45 | return ((x & y) ^ (~x & z));
46 | }
47 | function majority(x,y,z) {
48 | return ((x & y) ^ (x & z) ^ (y & z));
49 | }
50 | function sha256_Sigma0(x) {
51 | return (rotateRight(2, x) ^ rotateRight(13, x) ^ rotateRight(22, x));
52 | }
53 | function sha256_Sigma1(x) {
54 | return (rotateRight(6, x) ^ rotateRight(11, x) ^ rotateRight(25, x));
55 | }
56 | function sha256_sigma0(x) {
57 | return (rotateRight(7, x) ^ rotateRight(18, x) ^ (x >>> 3));
58 | }
59 | function sha256_sigma1(x) {
60 | return (rotateRight(17, x) ^ rotateRight(19, x) ^ (x >>> 10));
61 | }
62 | function sha256_expand(W, j) {
63 | return (W[j&0x0f] += sha256_sigma1(W[(j+14)&0x0f]) + W[(j+9)&0x0f] +
64 | sha256_sigma0(W[(j+1)&0x0f]));
65 | }
66 |
67 | /* Hash constant words K: */
68 | var K256 = new Array(
69 | 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
70 | 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
71 | 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
72 | 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
73 | 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
74 | 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
75 | 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
76 | 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
77 | 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
78 | 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
79 | 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
80 | 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
81 | 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
82 | 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
83 | 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
84 | 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
85 | );
86 |
87 | /* global arrays */
88 | var ihash, count, buffer;
89 | var sha256_hex_digits = "0123456789abcdef";
90 |
91 | /* Add 32-bit integers with 16-bit operations (bug in some JS-interpreters:
92 | overflow) */
93 | function safe_add(x, y)
94 | {
95 | var lsw = (x & 0xffff) + (y & 0xffff);
96 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
97 | return (msw << 16) | (lsw & 0xffff);
98 | }
99 |
100 | /* Initialise the SHA256 computation */
101 | function sha256_init() {
102 | ihash = new Array(8);
103 | count = new Array(2);
104 | buffer = new Array(64);
105 | count[0] = count[1] = 0;
106 | ihash[0] = 0x6a09e667;
107 | ihash[1] = 0xbb67ae85;
108 | ihash[2] = 0x3c6ef372;
109 | ihash[3] = 0xa54ff53a;
110 | ihash[4] = 0x510e527f;
111 | ihash[5] = 0x9b05688c;
112 | ihash[6] = 0x1f83d9ab;
113 | ihash[7] = 0x5be0cd19;
114 | }
115 |
116 | /* Transform a 512-bit message block */
117 | function sha256_transform() {
118 | var a, b, c, d, e, f, g, h, T1, T2;
119 | var W = new Array(16);
120 |
121 | /* Initialize registers with the previous intermediate value */
122 | a = ihash[0];
123 | b = ihash[1];
124 | c = ihash[2];
125 | d = ihash[3];
126 | e = ihash[4];
127 | f = ihash[5];
128 | g = ihash[6];
129 | h = ihash[7];
130 |
131 | /* make 32-bit words */
132 | for(var i=0; i<16; i++)
133 | W[i] = ((buffer[(i<<2)+3]) | (buffer[(i<<2)+2] << 8) | (buffer[(i<<2)+1]
134 | << 16) | (buffer[i<<2] << 24));
135 |
136 | for(var j=0; j<64; j++) {
137 | T1 = h + sha256_Sigma1(e) + choice(e, f, g) + K256[j];
138 | if(j < 16) T1 += W[j];
139 | else T1 += sha256_expand(W, j);
140 | T2 = sha256_Sigma0(a) + majority(a, b, c);
141 | h = g;
142 | g = f;
143 | f = e;
144 | e = safe_add(d, T1);
145 | d = c;
146 | c = b;
147 | b = a;
148 | a = safe_add(T1, T2);
149 | }
150 |
151 | /* Compute the current intermediate hash value */
152 | ihash[0] += a;
153 | ihash[1] += b;
154 | ihash[2] += c;
155 | ihash[3] += d;
156 | ihash[4] += e;
157 | ihash[5] += f;
158 | ihash[6] += g;
159 | ihash[7] += h;
160 | }
161 |
162 | /* Read the next chunk of data and update the SHA256 computation */
163 | function sha256_update(data, inputLen) {
164 | var i, index, curpos = 0;
165 | /* Compute number of bytes mod 64 */
166 | index = ((count[0] >> 3) & 0x3f);
167 | var remainder = (inputLen & 0x3f);
168 |
169 | /* Update number of bits */
170 | if ((count[0] += (inputLen << 3)) < (inputLen << 3)) count[1]++;
171 | count[1] += (inputLen >> 29);
172 |
173 | /* Transform as many times as possible */
174 | for(i=0; i+63> 3) & 0x3f);
189 | buffer[index++] = 0x80;
190 | if(index <= 56) {
191 | for(var i=index; i<56; i++)
192 | buffer[i] = 0;
193 | } else {
194 | for(var i=index; i<64; i++)
195 | buffer[i] = 0;
196 | sha256_transform();
197 | for(var i=0; i<56; i++)
198 | buffer[i] = 0;
199 | }
200 | buffer[56] = (count[1] >>> 24) & 0xff;
201 | buffer[57] = (count[1] >>> 16) & 0xff;
202 | buffer[58] = (count[1] >>> 8) & 0xff;
203 | buffer[59] = count[1] & 0xff;
204 | buffer[60] = (count[0] >>> 24) & 0xff;
205 | buffer[61] = (count[0] >>> 16) & 0xff;
206 | buffer[62] = (count[0] >>> 8) & 0xff;
207 | buffer[63] = count[0] & 0xff;
208 | sha256_transform();
209 | }
210 |
211 | /* Split the internal hash values into an array of bytes */
212 | function sha256_encode_bytes() {
213 | var j=0;
214 | var output = new Array(32);
215 | for(var i=0; i<8; i++) {
216 | output[j++] = ((ihash[i] >>> 24) & 0xff);
217 | output[j++] = ((ihash[i] >>> 16) & 0xff);
218 | output[j++] = ((ihash[i] >>> 8) & 0xff);
219 | output[j++] = (ihash[i] & 0xff);
220 | }
221 | return output;
222 | }
223 |
224 | /* Get the internal hash as a hex string */
225 | function sha256_encode_hex() {
226 | var output = new String();
227 | for(var i=0; i<8; i++) {
228 | for(var j=28; j>=0; j-=4)
229 | output += sha256_hex_digits.charAt((ihash[i] >>> j) & 0x0f);
230 | }
231 | return output;
232 | }
233 |
234 | /* Main function: returns a hex string representing the SHA256 value of the
235 | given data */
236 | function sha256_digest(data) {
237 | sha256_init();
238 | sha256_update(data, data.length);
239 | sha256_final();
240 | return sha256_encode_hex();
241 | }
242 |
243 | /* test if the JS-interpreter is working properly */
244 | function sha256_self_test()
245 | {
246 | return sha256_digest("message digest") ==
247 | "f7846f55cf23e14eebeab5b4e1550cad5b509e3348fbc4efa3a1413d393cb650";
248 | }
249 |
250 |
251 |
--------------------------------------------------------------------------------