├── .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 | 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 = $("" + 98 | ""); 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 | --------------------------------------------------------------------------------