├── README.md
├── configuration.yaml
├── custom_components
├── media_player
│ └── floorplan_speaker.py
└── sensor
│ └── http_room.py
├── customize.yaml
├── panel_custom.yaml
├── panels
├── clock.html
└── floorplan.html
└── www
└── custom_ui
├── floorplan
├── alarm_clock.svg
├── alarm_clock.yaml
├── ha-floorplan.html
└── lib
│ ├── floorplan.js
│ ├── fully-kiosk.js
│ ├── jquery-3.3.1.min.js
│ ├── moment.min.js
│ └── yaml.min.js
└── state-card-floorplan.html
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ⚠️ Please migrate to ha-floorplan ⚠️
6 |
7 |
8 | ha-floorplan has been replaced with ha-floorplan.
Please check out the new solution, and let us know what you think.
9 |
10 | |
11 |
12 |
13 |
14 | # ha-floorplan-kiosk
15 | 1) Update to the latest version of Floorplan (with support for Fully Kiosk) by copying the following file to the `www/custom_ui/floorplan` folder of Home Assistant:
16 |
17 | ```
18 | https://raw.githubusercontent.com/pkozul/ha-floorplan-kiosk/master/floorplan.yaml
19 | ```
20 |
21 | 2) Copy Fully Kiosk library to the `www/custom_ui/floorplan/lib` folder of Home Assistant:
22 |
23 | ```
24 | https://raw.githubusercontent.com/pkozul/ha-floorplan-kiosk/master/www/custom_ui/floorplan/lib/fully-kiosk.js
25 | ```
26 |
27 | 3) Copy the following file to the `custom_components/media_player` folder of Home Assistant:
28 |
29 | ```
30 | https://raw.githubusercontent.com/pkozul/ha-floorplan-kiosk/master/custom_components/media_player/floorplan_speaker.py
31 | ```
32 |
33 | 4) For each Fully Kiosk device, create a binary sensor entity:
34 |
35 | ```
36 | binary_sensor:
37 |
38 | - platform: mqtt
39 | state_topic: floorplan/kiosk/entry
40 | name: Entry Kiosk
41 | retain: true
42 |
43 | - platform: mqtt
44 | state_topic: floorplan/kiosk/bedroom
45 | name: Bedroom Kiosk
46 | retain: true
47 | ```
48 |
49 | 5) For each Fully Kiosk device, create a media player entity:
50 |
51 | ```
52 | media_player:
53 |
54 | - platform: floorplan_speaker
55 | name: Entry Kiosk
56 |
57 | - platform: floorplan_speaker
58 | name: Bedroom Kiosk
59 | ```
60 |
61 | 6) In the floorplan configuration, enable Fully Kiosk support by adding `fully_kiosk:`. Then create a `devices` section, and add each Fully Kiosk device to it. Make sure to specify the correct MAC address for each Fully Kiosk device.
62 |
63 | ```
64 | name: Floorplan
65 | image: /local/custom_ui/floorplan/floorplan.svg
66 | stylesheet: /local/custom_ui/floorplan/floorplan.css
67 |
68 | warnings:
69 | debug:
70 | fully_kiosk:
71 |
72 | devices:
73 |
74 | fully_kiosk:
75 |
76 | - name: Entry Kiosk
77 | address: 00:FC:8B:4A:D5:CF
78 | motion_sensor: binary_sensor.entry_kiosk_motion
79 | plugged_sensor: binary_sensor.entry_kiosk_plugged
80 | media_player: media_player.entry_kiosk
81 | ```
82 |
83 | 7) Restart Home Assistant. The Fully Kiosk device should now be available as a binary sensor, and as a media player.
84 |
85 | 8) To test the TTS on the Fully Kiosk device, call the TTS play service:
86 |
87 | ```
88 | service: tts.google_say
89 | entity_id: media_player.entry_kiosk
90 | data:
91 | message: 'May the Force be with you.'
92 | ```
93 |
--------------------------------------------------------------------------------
/configuration.yaml:
--------------------------------------------------------------------------------
1 |
2 | binary_sensor:
3 | - platform: mqtt
4 | state_topic: dummy/binary_sensor
5 | name: Floorplan
6 |
7 | - platform: mqtt
8 | state_topic: floorplan/kiosk/entry/motion
9 | name: Entry Kiosk Motion
10 | retain: true
11 |
12 | - platform: mqtt
13 | state_topic: floorplan/kiosk/entry/plugged
14 | name: Entry Kiosk Plugged
15 | retain: true
16 |
17 | - platform: mqtt
18 | state_topic: floorplan/kiosk/bedroom/motion
19 | name: Bedroom Kiosk Motion
20 | retain: true
21 |
22 | - platform: mqtt
23 | state_topic: floorplan/kiosk/bedroom/plugged
24 | name: Bedroom Kiosk Plugged
25 | retain: true
26 |
27 | media_player:
28 | - platform: floorplan_speaker
29 | name: Entry Kiosk
30 |
31 | - platform: floorplan_speaker
32 | name: Bedroom Kiosk
33 |
34 |
--------------------------------------------------------------------------------
/custom_components/media_player/floorplan_speaker.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for Floorplan Speaker
3 |
4 | """
5 | import voluptuous as vol
6 |
7 | from homeassistant.components.media_player import (
8 | ENTITY_ID_FORMAT,
9 | SUPPORT_PLAY_MEDIA,
10 | SUPPORT_VOLUME_SET,
11 | PLATFORM_SCHEMA,
12 | MediaPlayerDevice)
13 | from homeassistant.const import (
14 | CONF_NAME, STATE_IDLE, STATE_PLAYING)
15 | from homeassistant.components import http
16 | from homeassistant.components.http import HomeAssistantView
17 | from homeassistant.helpers.entity import async_generate_entity_id
18 | import homeassistant.helpers.config_validation as cv
19 |
20 | import logging
21 |
22 | import os
23 | import re
24 | import sys
25 | import time
26 | import asyncio
27 | import json
28 |
29 | DEFAULT_NAME = 'Floorplan Speaker'
30 | DEFAULT_VOLUME = 1.0
31 |
32 | SUPPORT_FLOORPLAN_SPEAKER = SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET
33 |
34 | CONF_ADDRESS = 'address'
35 |
36 | ATTR_ADDRESS = 'address'
37 | ATTR_BATTERY_LEVEL = 'battery_level'
38 | ATTR_SCREEN_BRIGHTNESS = 'screen_brightness'
39 | ATTR_DEVICE_ID = 'device_id'
40 | ATTR_SERIAL_NUMBER = 'serial_number'
41 |
42 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
43 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
44 | })
45 |
46 | _LOGGER = logging.getLogger(__name__)
47 |
48 | def setup_platform(hass, config, add_devices, discovery_info=None):
49 | name = config.get(CONF_NAME)
50 | address = config.get(CONF_ADDRESS)
51 |
52 | device = FloorplanSpeakerDevice(hass, name, address)
53 |
54 | """Set up an endpoint for the media player."""
55 | hass.http.register_view(device)
56 |
57 | add_devices([device])
58 |
59 | return True
60 |
61 | class FloorplanSpeakerDevice(MediaPlayerDevice, http.HomeAssistantView):
62 | def __init__(self, hass, name, address):
63 | self._hass = hass
64 | self._name = name
65 | self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
66 | self._state = STATE_IDLE
67 | self._media_content_id = None
68 | self._address = address
69 | self._volume = DEFAULT_VOLUME
70 | self._battery_level = None
71 | self._screen_brightness = None
72 | self._device_id = None
73 | self._serial_number = None
74 | self.url = '/api/fully_kiosk/media_player/' + self.entity_id
75 | _LOGGER.info('Setting endpoint: %s', self.url)
76 |
77 | @asyncio.coroutine
78 | def post(self, request):
79 | body = yield from request.text()
80 | try:
81 | data = json.loads(body) if body else None
82 | except ValueError:
83 | return self.json_message('Event data should be valid JSON', HTTP_BAD_REQUEST)
84 |
85 | if data is not None and not isinstance(data, dict):
86 | return self.json_message('Event data should be a JSON object', HTTP_BAD_REQUEST)
87 |
88 | data = json.loads(body) if body else None
89 |
90 | _LOGGER.info("Received from Fully Kiosk: %s: %s", self.url, data)
91 |
92 | self._state = data['state']
93 | self._media_content_id = data['attributes']['media_content_id']
94 | self._volume = data['attributes']['volume_level']
95 | self._address = data['attributes'][ATTR_ADDRESS]
96 | self._battery_level = data['attributes'][ATTR_BATTERY_LEVEL]
97 | self._screen_brightness = data['attributes'][ATTR_SCREEN_BRIGHTNESS]
98 | self._device_id = data['attributes'][ATTR_DEVICE_ID]
99 | self._serial_number = data['attributes'][ATTR_SERIAL_NUMBER]
100 |
101 | @property
102 | def name(self):
103 | return self._name
104 |
105 | @property
106 | def state(self):
107 | return self._state
108 |
109 | @property
110 | def supported_features(self):
111 | return SUPPORT_FLOORPLAN_SPEAKER
112 |
113 | @property
114 | def address(self):
115 | return self._address
116 |
117 | @property
118 | def volume_level(self):
119 | return self._volume
120 |
121 | @property
122 | def media_content_id(self):
123 | return self._media_content_id
124 |
125 | @property
126 | def battery_level(self):
127 | return self._battery_level
128 |
129 | @property
130 | def device_id(self):
131 | return self._device_id
132 |
133 | @property
134 | def serial_number(self):
135 | return self._serial_number
136 |
137 | @property
138 | def device_state_attributes(self):
139 | return {
140 | ATTR_ADDRESS: self._address,
141 | ATTR_BATTERY_LEVEL: self._battery_level,
142 | ATTR_SCREEN_BRIGHTNESS: self._screen_brightness,
143 | ATTR_DEVICE_ID: self._device_id,
144 | ATTR_SERIAL_NUMBER: self._serial_number,
145 | }
146 |
147 | def set_volume_level(self, volume):
148 | self._volume = volume
149 |
150 | def play_media(self, media_type, media_id, **kwargs):
151 | _LOGGER.info('play_media: %s', media_id)
152 |
153 | def media_play(self):
154 | _LOGGER.info('media_play')
155 |
156 | def media_pause(self):
157 | _LOGGER.info('media_pause')
158 |
159 | def media_stop(self):
160 | _LOGGER.info('media_stop')
161 |
--------------------------------------------------------------------------------
/custom_components/sensor/http_room.py:
--------------------------------------------------------------------------------
1 | """
2 | Support for HTTP room presence detection.
3 |
4 | For more details about this platform, please refer to the documentation at
5 | https://home-assistant.io/components/sensor.http_room/
6 | """
7 | import asyncio
8 | import logging
9 | import json
10 | from datetime import timedelta
11 |
12 | from aiohttp.web_exceptions import HTTPInternalServerError
13 |
14 | from homeassistant.components.http import HomeAssistantView
15 |
16 | import voluptuous as vol
17 |
18 | import homeassistant.helpers.config_validation as cv
19 | from homeassistant.components.sensor import PLATFORM_SCHEMA
20 | from homeassistant.const import (
21 | CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME, ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE)
22 | from homeassistant.core import callback
23 | from homeassistant.helpers.entity import Entity
24 | from homeassistant.util import dt, slugify
25 |
26 |
27 | _LOGGER = logging.getLogger(__name__)
28 |
29 | DEPENDENCIES = ['http']
30 |
31 | ATTR_DEVICE_ID = 'device_id'
32 | ATTR_DISTANCE = 'distance'
33 | ATTR_ROOM = 'room'
34 | ATTR_UUID = 'uuid'
35 | ATTR_MAJOR = 'major'
36 | ATTR_MINOR = 'minor'
37 |
38 | CONF_DEVICE_ID = 'device_id'
39 | CONF_AWAY_TIMEOUT = 'away_timeout'
40 |
41 | DEFAULT_AWAY_TIMEOUT = 0
42 | DEFAULT_NAME = 'Room Sensor'
43 | DEFAULT_TIMEOUT = 5
44 |
45 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
46 | vol.Required(CONF_DEVICE_ID): cv.string,
47 | vol.Required(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
48 | vol.Optional(CONF_AWAY_TIMEOUT,
49 | default=DEFAULT_AWAY_TIMEOUT): cv.positive_int,
50 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
51 | })
52 |
53 |
54 |
55 | @asyncio.coroutine
56 | def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
57 | """Set up HTTP room Sensor."""
58 | sensor = HttpRoomSensor(
59 | config.get(CONF_NAME),
60 | config.get(CONF_DEVICE_ID),
61 | config.get(CONF_TIMEOUT),
62 | config.get(CONF_AWAY_TIMEOUT)
63 | )
64 |
65 | async_add_devices([sensor])
66 |
67 | hass.http.register_view(sensor)
68 |
69 |
70 | class HttpRoomSensor(HomeAssistantView, Entity):
71 | """Representation of a room sensor that is updated via HTTP."""
72 | """View to handle Room Presence HTTP requests."""
73 |
74 | url = '/api/room_presence/{device_id}'
75 |
76 | def __init__(self, name, device_id, timeout, consider_home):
77 | """Initialize the sensor."""
78 | self._state = STATE_NOT_HOME
79 | self._name = name
80 | self._device_id = slugify(device_id).upper()
81 | self._timeout = timeout
82 | self._consider_home = \
83 | timedelta(seconds=consider_home) if consider_home \
84 | else None
85 | self._distance = None
86 | self._uuid = None
87 | self._major = None
88 | self._minor = None
89 | self._latitude = None
90 | self._longitude = None
91 | self._updated = None
92 |
93 | def update_state(self, device_id, room, distance, uuid, major, minor, latitude, longitude):
94 | """Update the sensor state."""
95 | self._state = room
96 | self._distance = distance
97 | self._uuid = uuid
98 | self._major = major
99 | self._minor = minor
100 | self._latitude = latitude
101 | self._longitude = longitude
102 | self._updated = dt.utcnow()
103 | _LOGGER.info('Updated device presence: %s', device_id)
104 |
105 | self.async_schedule_update_ha_state()
106 |
107 | def message_received(self, device):
108 | if device.get(CONF_DEVICE_ID) == self._device_id:
109 | if self._distance is None or self._updated is None:
110 | self.update_state(**device)
111 | else:
112 | # update if:
113 | # device is in the same room OR
114 | # device is closer to another room OR
115 | # last update from other room was too long ago
116 | timediff = dt.utcnow() - self._updated
117 | if device.get(ATTR_ROOM) == self._state \
118 | or device.get(ATTR_DISTANCE) < self._distance \
119 | or timediff.seconds >= self._timeout:
120 | self.update_state(**device)
121 |
122 | @asyncio.coroutine
123 | def post(self, request, device_id):
124 | """Handle a Room Presence message."""
125 | hass = request.app['hass']
126 |
127 | data = yield from request.json()
128 |
129 | room = data.get(ATTR_ROOM)
130 | distance = data.get(ATTR_DISTANCE)
131 | uuid = data.get(ATTR_UUID)
132 | major = data.get(ATTR_MAJOR)
133 | minor = data.get(ATTR_MINOR)
134 | latitude = data.get(ATTR_LATITUDE)
135 | longitude = data.get(ATTR_LONGITUDE)
136 |
137 | device = {
138 | ATTR_DEVICE_ID: device_id,
139 | ATTR_ROOM: room,
140 | ATTR_DISTANCE: distance,
141 | ATTR_UUID: uuid,
142 | ATTR_MAJOR: major,
143 | ATTR_MINOR: minor,
144 | ATTR_LATITUDE: latitude,
145 | ATTR_LONGITUDE: longitude
146 | }
147 |
148 | _LOGGER.info('Received presence data: %s', device)
149 |
150 | try:
151 | self.message_received(device)
152 | return self.json([])
153 |
154 | except ValueError:
155 | raise HTTPInternalServerError
156 |
157 | @property
158 | def name(self):
159 | """Return the name of the sensor."""
160 | return self._name
161 |
162 | @property
163 | def device_state_attributes(self):
164 | """Return the state attributes."""
165 | return {
166 | ATTR_DISTANCE: self._distance,
167 | ATTR_UUID: self._uuid,
168 | ATTR_MAJOR: self._major,
169 | ATTR_MINOR: self._minor,
170 | ATTR_LATITUDE: self._latitude,
171 | ATTR_LONGITUDE: self._longitude
172 | }
173 |
174 | @property
175 | def state(self):
176 | """Return the current room of the entity."""
177 | return self._state
178 |
179 | def update(self):
180 | """Update the state for absent devices."""
181 | if self._updated \
182 | and self._consider_home \
183 | and dt.utcnow() - self._updated > self._consider_home:
184 | self._state = STATE_NOT_HOME
185 |
186 |
187 | def _parse_update_data(topic, data):
188 | """Parse the room presence update."""
189 | parts = topic.split('/')
190 | room = parts[-1]
191 | device_id = slugify(data.get(ATTR_ID)).upper()
192 | distance = data.get(ATTR_DISTANCE)
193 | parsed_data = {
194 | ATTR_DEVICE_ID: device_id,
195 | ATTR_ROOM: room,
196 | ATTR_DISTANCE: distance
197 | }
198 | return parsed_data
199 |
--------------------------------------------------------------------------------
/customize.yaml:
--------------------------------------------------------------------------------
1 | binary_sensor.floorplan:
2 | custom_ui_state_card: state-card-floorplan
3 | config:
4 | config: /local/custom_ui/floorplan/floorplan.yaml
5 |
--------------------------------------------------------------------------------
/panel_custom.yaml:
--------------------------------------------------------------------------------
1 |
2 | - name: floorplan
3 | sidebar_title: Floorplan
4 | sidebar_icon: mdi:home
5 | url_path: floorplan
6 | config:
7 | hide_app_toolbar:
8 | config: /local/custom_ui/floorplan/floorplan.yaml
9 |
10 | - name: clock
11 | sidebar_title: Alarm Clock
12 | sidebar_icon: mdi:alarm
13 | url_path: clock
14 | config:
15 | hide_app_toolbar:
16 | config: /local/custom_ui/floorplan/clock.yaml
17 |
18 |
--------------------------------------------------------------------------------
/panels/clock.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
19 | [[panel.title]]
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
59 |
--------------------------------------------------------------------------------
/panels/floorplan.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
23 |
24 |
25 |
26 | [[panel.title]]
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
78 |
--------------------------------------------------------------------------------
/www/custom_ui/floorplan/alarm_clock.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
198 |
--------------------------------------------------------------------------------
/www/custom_ui/floorplan/alarm_clock.yaml:
--------------------------------------------------------------------------------
1 | page_id: alarm_clock
2 | image: /local/custom_ui/floorplan/alarm_clock.svg
3 | #stylesheet: /local/custom_ui/floorplan/alarm_clock.css
4 |
5 | startup:
6 | actions:
7 | - service: floorplan.variable_set
8 | data:
9 | variable: floorplan.hours
10 | value_template: 'return parseInt(entities["input_datetime.alarm_time"].state.slice(0, 2));'
11 | - service: floorplan.variable_set
12 | data:
13 | variable: floorplan.minutes
14 | value_template: 'return parseInt(entities["input_datetime.alarm_time"].state.slice(3, 5));'
15 |
16 | rules:
17 | - entity: input_datetime.alarm_time
18 | text_template: '${entity.state.slice(0, 5)}'
19 |
20 | - entities:
21 | - floorplan.hours
22 | - floorplan.minutes
23 | text_template: '${("0" + entity.state).slice(-2)}'
24 |
25 | - entity: floorplan.hours
26 | element: input_number.alarm_time_hours_up
27 | action:
28 | service: floorplan.variable_set
29 | data:
30 | variable: floorplan.hours
31 | value_template: 'return (entity.state < 23) ? entity.state + 1 : entity.state;'
32 |
33 | - entity: floorplan.hours
34 | element: input_number.alarm_time_hours_down
35 | action:
36 | service: floorplan.variable_set
37 | data:
38 | variable: floorplan.hours
39 | value_template: 'return (entity.state > 0) ? entity.state - 1 : entity.state;'
40 |
41 | - entity: floorplan.minutes
42 | element: input_number.alarm_time_minutes_up
43 | action:
44 | service: floorplan.variable_set
45 | data:
46 | variable: floorplan.minutes
47 | value_template: '${((parseInt(entity.state / 5) * 5) + 5) % 60}'
48 |
49 | - entity: floorplan.minutes
50 | element: input_number.alarm_time_minutes_down
51 | action:
52 | service: floorplan.variable_set
53 | data:
54 | variable: floorplan.minutes
55 | value_template: '${(((parseInt(entity.state / 5) * 5) - 5) + 60) % 60}'
56 |
57 | - element: save_alarm_time_button
58 | action:
59 | service: input_datetime.set_datetime
60 | data_template: '{ "entity_id": "input_datetime.alarm_time", "time": "${entities[`floorplan.hours`].state}:${entities[`floorplan.minutes`].state}" }'
61 |
--------------------------------------------------------------------------------
/www/custom_ui/floorplan/ha-floorplan.html:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
63 |
64 |
65 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
208 |
--------------------------------------------------------------------------------
/www/custom_ui/floorplan/lib/fully-kiosk.js:
--------------------------------------------------------------------------------
1 | /*
2 | Floorplan Fully Kiosk for Home Assistant
3 | Version: 1.0.7.50
4 | By Petar Kozul
5 | https://github.com/pkozul/ha-floorplan
6 | */
7 |
8 | 'use strict';
9 |
10 | (function () {
11 | if (typeof window.FullyKiosk === 'function') {
12 | return;
13 | }
14 |
15 | class FullyKiosk {
16 | constructor(floorplan) {
17 | this.version = '1.0.7.50';
18 |
19 | this.floorplan = floorplan;
20 | this.authToken = (window.localStorage && window.localStorage.authToken) ? window.localStorage.authToken : '';
21 |
22 | this.fullyInfo = {};
23 | this.fullyState = {};
24 | this.beacons = {};
25 |
26 | this.throttledFunctions = {};
27 | }
28 |
29 | /***************************************************************************************************************************/
30 | /* Initialization
31 | /***************************************************************************************************************************/
32 |
33 | init() {
34 | this.logInfo('VERSION', `Fully Kiosk v${this.version}`);
35 |
36 | /*
37 | let uuid = 'a445425b-c718-461c-a876-aa647abd99d4';
38 | let deviceId = uuid.replace(/[-_]/g, '').toUpperCase();
39 | let payload = { room: 'entry hall', id: uuid, distance: 123.45 };
40 | this.PostToHomeAssistant(`/api/room_presence/${deviceId}`, payload);
41 | */
42 |
43 | if (typeof fully === "undefined") {
44 | this.logInfo('FULLY_KIOSK', `Fully Kiosk is not running or not enabled. You can enable it via Settings > Other Settings > Enable Website Integration (PLUS).`);
45 | return;
46 | }
47 |
48 | let macAddress = fully.getMacAddress().toLowerCase();
49 |
50 | let device = this.floorplan.config && this.floorplan.config.fully_kiosk &&
51 | this.floorplan.config.fully_kiosk.find(x => x.address.toLowerCase() == macAddress);
52 | if (!device) {
53 | return;
54 | }
55 |
56 | if (!navigator.geolocation) {
57 | this.logInfo('FULLY_KIOSK', "Geolocation is not supported or not enabled. You can enable it via Settings > Web Content Settings > Enable Geolocation Access (PLUS) and on the device via Google Settings > Location > Fully Kiosk Browser.");
58 | }
59 |
60 | this.fullyInfo = this.getFullyInfo(device);
61 |
62 | this.updateFullyState();
63 | this.updateCurrentPosition();
64 |
65 | this.initAudio();
66 | this.addAudioEventHandlers();
67 | this.addFullyEventHandlers();
68 | this.subscribeHomeAssistantEvents();
69 |
70 | this.sendMotionState();
71 | this.sendPluggedState();
72 | this.sendScreensaverState();
73 | this.sendMediaPlayerState();
74 | }
75 |
76 | initAudio() {
77 | this.audio = new Audio();
78 | this.isAudioPlaying = false;
79 | }
80 |
81 | getFullyInfo(device) {
82 | return {
83 | motionBinarySensorEntityId: device.motion_sensor,
84 | pluggedBinarySensorEntityId: device.plugged_sensor,
85 | screensaverLightEntityId: device.screensaver_light,
86 | mediaPlayerEntityId: device.media_player,
87 |
88 | locationName: device.presence_detection ? device.presence_detection.location_name : undefined,
89 |
90 | startUrl: fully.getStartUrl(),
91 | currentLocale: fully.getCurrentLocale(),
92 | ipAddressv4: fully.getIp4Address(),
93 | ipAddressv6: fully.getIp6Address(),
94 | macAddress: fully.getMacAddress(),
95 | wifiSSID: fully.getWifiSsid(),
96 | serialNumber: fully.getSerialNumber(),
97 | deviceId: fully.getDeviceId(),
98 |
99 | isMotionDetected: false,
100 | isScreensaverOn: false,
101 |
102 | supportsGeolocation: (navigator.geolocation != undefined),
103 | };
104 | }
105 |
106 | updateFullyState() {
107 | this.fullyState.batteryLevel = fully.getBatteryLevel();
108 | this.fullyState.screenBrightness = fully.getScreenBrightness();
109 | this.fullyState.isScreenOn = fully.getScreenOn();
110 | this.fullyState.isPluggedIn = fully.isPlugged();
111 | }
112 |
113 | /***************************************************************************************************************************/
114 | /* Set up event handlers
115 | /***************************************************************************************************************************/
116 |
117 | addAudioEventHandlers() {
118 | this.audio.addEventListener('play', this.onAudioPlay.bind(this));
119 | this.audio.addEventListener('playing', this.onAudioPlaying.bind(this));
120 | this.audio.addEventListener('pause', this.onAudioPause.bind(this));
121 | this.audio.addEventListener('ended', this.onAudioEnded.bind(this));
122 | this.audio.addEventListener('volumechange', this.onAudioVolumeChange.bind(this));
123 | }
124 |
125 | addFullyEventHandlers() {
126 | window['onFullyEvent'] = (e) => { window.dispatchEvent(new Event(e)); }
127 |
128 | window['onFullyIBeaconEvent'] = (e, uuid, major, minor, distance) => {
129 | let event = new CustomEvent(e, {
130 | detail: { uuid: uuid, major: major, minor: minor, distance: distance, timestamp: new Date() }
131 | });
132 | window.dispatchEvent(event);
133 | }
134 |
135 | window.addEventListener('fully.screenOn', this.onScreenOn.bind(this));
136 | window.addEventListener('fully.screenOff', this.onScreenOff.bind(this));
137 | window.addEventListener('fully.networkDisconnect', this.onNetworkDisconnect.bind(this));
138 | window.addEventListener('fully.networkReconnect', this.onNetworkReconnect.bind(this));
139 | window.addEventListener('fully.internetDisconnect', this.onInternetDisconnect.bind(this));
140 | window.addEventListener('fully.internetReconnect', this.onInternetReconnect.bind(this));
141 | window.addEventListener('fully.unplugged', this.onUnplugged.bind(this));
142 | window.addEventListener('fully.pluggedAC', this.onPluggedAC.bind(this));
143 | window.addEventListener('fully.pluggedUSB', this.onPluggedUSB.bind(this));
144 | window.addEventListener('fully.onScreensaverStart', this.onScreensaverStart.bind(this));
145 | window.addEventListener('fully.onScreensaverStop', this.onScreensaverStop.bind(this));
146 | window.addEventListener('fully.onBatteryLevelChanged', this.onBatteryLevelChanged.bind(this));
147 | window.addEventListener('fully.onMotion', this.onMotion.bind(this));
148 |
149 | if (this.fullyInfo.supportsGeolocation) {
150 | window.addEventListener('fully.onMovement', this.onMovement.bind(this));
151 | }
152 |
153 | if (this.fullyInfo.locationName) {
154 | this.logInfo('KIOSK', 'Listening for beacon messages');
155 | window.addEventListener('fully.onIBeacon', this.onIBeacon.bind(this));
156 | }
157 |
158 | fully.bind('screenOn', 'onFullyEvent("fully.screenOn");')
159 | fully.bind('screenOff', 'onFullyEvent("fully.screenOff");')
160 | fully.bind('networkDisconnect', 'onFullyEvent("fully.networkDisconnect");')
161 | fully.bind('networkReconnect', 'onFullyEvent("fully.networkReconnect");')
162 | fully.bind('internetDisconnect', 'onFullyEvent("fully.internetDisconnect");')
163 | fully.bind('internetReconnect', 'onFullyEvent("fully.internetReconnect");')
164 | fully.bind('unplugged', 'onFullyEvent("fully.unplugged");')
165 | fully.bind('pluggedAC', 'onFullyEvent("fully.pluggedAC");')
166 | fully.bind('pluggedUSB', 'onFullyEvent("fully.pluggedUSB");')
167 | fully.bind('onScreensaverStart', 'onFullyEvent("fully.onScreensaverStart");')
168 | fully.bind('onScreensaverStop', 'onFullyEvent("fully.onScreensaverStop");')
169 | fully.bind('onBatteryLevelChanged', 'onFullyEvent("fully.onBatteryLevelChanged");')
170 | fully.bind('onMotion', 'onFullyEvent("fully.onMotion");') // Max. one per second
171 | fully.bind('onMovement', 'onFullyEvent("fully.onMovement");')
172 | fully.bind('onIBeacon', 'onFullyIBeaconEvent("fully.onIBeacon", "$id1", "$id2", "$id3", $distance);')
173 | }
174 |
175 | /***************************************************************************************************************************/
176 | /* Fully Kiosk events
177 | /***************************************************************************************************************************/
178 |
179 | onScreenOn() {
180 | this.logDebug('FULLY_KIOSK', 'Screen turned on');
181 | }
182 |
183 | onScreenOff() {
184 | this.logDebug('FULLY_KIOSK', 'Screen turned off');
185 | }
186 |
187 | onNetworkDisconnect() {
188 | this.logDebug('FULLY_KIOSK', 'Network disconnected');
189 | }
190 |
191 | onNetworkReconnect() {
192 | this.logDebug('FULLY_KIOSK', 'Network reconnected');
193 | }
194 |
195 | onInternetDisconnect() {
196 | this.logDebug('FULLY_KIOSK', 'Internet disconnected');
197 | }
198 |
199 | onInternetReconnect() {
200 | this.logDebug('FULLY_KIOSK', 'Internet reconnected');
201 | }
202 |
203 | onUnplugged() {
204 | this.logDebug('FULLY_KIOSK', 'Unplugged AC');
205 | this.fullyState.isPluggedIn = false;
206 | this.sendPluggedState();
207 | }
208 |
209 | onPluggedAC() {
210 | this.logDebug('FULLY_KIOSK', 'Plugged AC');
211 | this.fullyState.isPluggedIn = true;
212 | this.sendPluggedState();
213 | }
214 |
215 | onPluggedUSB() {
216 | this.logDebug('FULLY_KIOSK', 'Unplugged USB');
217 | this.logDebug('FULLY_KIOSK', 'Device plugged into USB');
218 | }
219 |
220 | onScreensaverStart() {
221 | this.fullyState.isScreensaverOn = true;
222 | this.logDebug('FULLY_KIOSK', 'Screensaver started');
223 | this.sendScreensaverState();
224 | }
225 |
226 | onScreensaverStop() {
227 | this.fullyState.isScreensaverOn = false;
228 | this.logDebug('FULLY_KIOSK', 'Screensaver stopped');
229 | this.sendScreensaverState();
230 | }
231 |
232 | onBatteryLevelChanged() {
233 | this.logDebug('FULLY_KIOSK', 'Battery level changed');
234 | }
235 |
236 | onMotion() {
237 | this.fullyState.isMotionDetected = true;
238 | this.logDebug('FULLY_KIOSK', 'Motion detected');
239 | this.sendMotionState();
240 | }
241 |
242 | onMovement(e) {
243 | let functionId = 'onMovement';
244 | let throttledFunc = this.throttledFunctions[functionId];
245 | if (!throttledFunc) {
246 | throttledFunc = this.throttle(this.onMovementThrottled.bind(this), 10000);
247 | this.throttledFunctions[functionId] = throttledFunc;
248 | }
249 |
250 | return throttledFunc(e);
251 | }
252 |
253 | onMovementThrottled() {
254 | this.logDebug('FULLY_KIOSK', 'Movement detected (throttled)');
255 |
256 | if (this.fullyInfo.supportsGeolocation) {
257 | this.updateCurrentPosition()
258 | .then(() => {
259 | this.sendMotionState();
260 | });
261 | }
262 | }
263 |
264 | onIBeacon(e) {
265 | let functionId = e.detail.uuid;
266 | let throttledFunc = this.throttledFunctions[functionId];
267 | if (!throttledFunc) {
268 | throttledFunc = this.throttle(this.onIBeaconThrottled.bind(this), 10000);
269 | this.throttledFunctions[functionId] = throttledFunc;
270 | }
271 |
272 | return throttledFunc(e);
273 | }
274 |
275 | onIBeaconThrottled(e) {
276 | let beacon = e.detail;
277 |
278 | this.logDebug('FULLY_KIOSK', `Received (throttled) beacon message (${JSON.stringify(beacon)})`);
279 |
280 | let beaconId = beacon.uuid;
281 | beaconId += (beacon.major ? `_${beacon.major}` : '');
282 | beaconId += (beacon.minor ? `_${beacon.minor}` : '');
283 |
284 | this.beacons[beaconId] = beacon;
285 |
286 | this.sendBeaconState(beacon);
287 | }
288 |
289 | /***************************************************************************************************************************/
290 | /* HTML5 Audio
291 | /***************************************************************************************************************************/
292 |
293 | onAudioPlay() {
294 | this.isAudioPlaying = true;
295 | this.sendMediaPlayerState();
296 | }
297 |
298 | onAudioPlaying() {
299 | this.isAudioPlaying = true;
300 | this.sendMediaPlayerState();
301 | }
302 |
303 | onAudioPause() {
304 | this.isAudioPlaying = false;
305 | this.sendMediaPlayerState();
306 | }
307 |
308 | onAudioEnded() {
309 | this.isAudioPlaying = false;
310 | this.sendMediaPlayerState();
311 | }
312 |
313 | onAudioVolumeChange() {
314 | this.sendMediaPlayerState();
315 | }
316 |
317 | /***************************************************************************************************************************/
318 | /* Send state to Home Assistant
319 | /***************************************************************************************************************************/
320 |
321 | sendMotionState() {
322 | if (!this.fullyInfo.motionBinarySensorEntityId) {
323 | return;
324 | }
325 |
326 | clearTimeout(this.sendMotionStateTimer);
327 | let timeout = this.fullyState.isMotionDetected ? 5000 : 10000;
328 |
329 | let state = this.fullyState.isMotionDetected ? "on" : "off";
330 | this.PostToHomeAssistant(`/api/states/${this.fullyInfo.motionBinarySensorEntityId}`, this.newPayload(state), () => {
331 | this.sendMotionStateTimer = setTimeout(() => {
332 | this.fullyState.isMotionDetected = false;
333 | this.sendMotionState();
334 |
335 | // Send other states as well
336 | this.sendPluggedState();
337 | this.sendScreensaverState();
338 | this.sendMediaPlayerState();
339 | }, timeout);
340 | });
341 | }
342 |
343 | sendPluggedState() {
344 | if (!this.fullyInfo.pluggedBinarySensorEntityId) {
345 | return;
346 | }
347 |
348 | let state = this.fullyState.isPluggedIn ? "on" : "off";
349 | this.PostToHomeAssistant(`/api/states/${this.fullyInfo.pluggedBinarySensorEntityId}`, this.newPayload(state));
350 | }
351 |
352 | sendScreensaverState() {
353 | if (!this.fullyInfo.screensaverLightEntityId) {
354 | return;
355 | }
356 |
357 | let state = this.fullyState.isScreensaverOn ? "on" : "off";
358 | this.PostToHomeAssistant(`/api/states/${this.fullyInfo.screensaverLightEntityId}`, this.newPayload(state));
359 | }
360 |
361 | sendMediaPlayerState() {
362 | if (!this.fullyInfo.mediaPlayerEntityId) {
363 | return;
364 | }
365 |
366 | let state = this.isAudioPlaying ? "playing" : "idle";
367 | this.PostToHomeAssistant(`/api/fully_kiosk/media_player/${this.fullyInfo.mediaPlayerEntityId}`, this.newPayload(state));
368 | }
369 |
370 | sendBeaconState(beacon) {
371 | if (!this.fullyInfo.motionBinarySensorEntityId) {
372 | return;
373 | }
374 |
375 | /*
376 | let payload = {
377 | name: this.fullyInfo.locationName,
378 | address: this.fullyInfo.macAddress,
379 | device: beacon.uuid,
380 | beaconUUID: beacon.uuid,
381 | latitude: this.position ? this.position.coords.latitude : undefined,
382 | longitude: this.position ? this.position.coords.longitude : undefined,
383 | entry: 1,
384 | }
385 | this.PostToHomeAssistant(`/api/geofency`, payload, undefined, false);
386 | */
387 |
388 | /*
389 | let payload = {
390 | mac: undefined,
391 | dev_id: beacon.uuid.replace(/-/g, '_'),
392 | host_name: undefined,
393 | location_name: this.fullyInfo.macAddress,
394 | gps: this.position ? [this.position.coords.latitude, this.position.coords.longitude] : undefined,
395 | gps_accuracy: undefined,
396 | battery: undefined,
397 |
398 | uuid: beacon.uuid,
399 | major: beacon.major,
400 | minor: beacon.minor,
401 | };
402 |
403 | this.PostToHomeAssistant(`/api/services/device_tracker/see`, payload);
404 | */
405 |
406 | /*
407 | let fullyId = this.fullyInfo.macAddress.replace(/[:-]/g, "_");
408 | payload = { topic: `room_presence/${fullyId}`, payload: `{ \"id\": \"${beacon.uuid}\", \"distance\": ${beacon.distance} }` };
409 | this.floorplan.hass.callService('mqtt', 'publish', payload);
410 | */
411 |
412 | let deviceId = beacon.uuid.replace(/[-_]/g, '').toUpperCase();
413 |
414 | let payload = {
415 | room: this.fullyInfo.locationName,
416 | uuid: beacon.uuid,
417 | major: beacon.major,
418 | minor: beacon.minor,
419 | distance: beacon.distance,
420 | latitude: this.position ? this.position.coords.latitude : undefined,
421 | longitude: this.position ? this.position.coords.longitude : undefined,
422 | };
423 |
424 | this.PostToHomeAssistant(`/api/room_presence/${deviceId}`, payload);
425 | }
426 |
427 | newPayload(state) {
428 | this.updateFullyState();
429 |
430 | let payload = {
431 | state: state,
432 | brightness: this.fullyState.screenBrightness,
433 | attributes: {
434 | volume_level: this.audio.volume,
435 | media_content_id: this.audio.src,
436 | address: this.fullyInfo.macAddress,
437 | mac_address: this.fullyInfo.macAddress,
438 | serial_number: this.fullyInfo.serialNumber,
439 | device_id: this.fullyInfo.deviceId,
440 | battery_level: this.fullyState.batteryLevel,
441 | screen_brightness: this.fullyState.screenBrightness,
442 | _isScreenOn: this.fullyState.isScreenOn,
443 | _isPluggedIn: this.fullyState.isPluggedIn,
444 | _isMotionDetected: this.fullyState.isMotionDetected,
445 | _isScreensaverOn: this.fullyState.isScreensaverOn,
446 | _latitude: this.position && this.position.coords.latitude,
447 | _longitude: this.position && this.position.coords.longitude,
448 | _beacons: JSON.stringify(Object.keys(this.beacons).map(beaconId => this.beacons[beaconId])),
449 | }
450 | };
451 |
452 | return payload;
453 | }
454 |
455 | /***************************************************************************************************************************/
456 | /* Geolocation
457 | /***************************************************************************************************************************/
458 |
459 | setScreenBrightness(brightness) {
460 | fully.setScreenBrightness(brightness);
461 | }
462 |
463 | startScreensaver() {
464 | this.logInfo('FULLY_KIOSK', `Starting screensaver`);
465 | fully.startScreensaver();
466 | }
467 |
468 | stopScreensaver() {
469 | this.logInfo('FULLY_KIOSK', `Stopping screensaver`);
470 | fully.stopScreensaver();
471 | }
472 |
473 | playTextToSpeech(text) {
474 | this.logInfo('FULLY_KIOSK', `Playing text-to-speech: ${text}`);
475 | fully.textToSpeech(text);
476 | }
477 |
478 | playMedia(mediaUrl) {
479 | this.audio.src = mediaUrl;
480 |
481 | this.logInfo('FULLY_KIOSK', `Playing media: ${this.audio.src}`);
482 | this.audio.play();
483 | }
484 |
485 | pauseMedia() {
486 | this.logInfo('FULLY_KIOSK', `Pausing media: ${this.audio.src}`);
487 | this.audio.pause();
488 | }
489 |
490 | setVolume(level) {
491 | this.audio.volume = level;
492 | }
493 |
494 | PostToHomeAssistant(url, payload, onSuccess) {
495 | let options = {
496 | type: 'POST',
497 | url: url,
498 | headers: { "X-HA-Access": this.authToken },
499 | data: JSON.stringify(payload),
500 | success: function (result) {
501 | this.logDebug('FULLY_KIOSK', `Posted state: ${url} ${JSON.stringify(payload)}`);
502 | if (onSuccess) {
503 | onSuccess();
504 | }
505 | }.bind(this),
506 | error: function (error) {
507 | this.handleError(new URIError(`Error posting state: ${url}`));
508 | }.bind(this)
509 | };
510 |
511 | jQuery.ajax(options);
512 | }
513 |
514 | subscribeHomeAssistantEvents() {
515 | /*
516 | this.floorplan.hass.connection.subscribeEvents((event) => {
517 | },
518 | 'state_changed');
519 | */
520 |
521 | this.floorplan.hass.connection.subscribeEvents((event) => {
522 | if (this.fullyInfo.screensaverLightEntityId && (event.data.domain === 'light')) {
523 | if (event.data.service_data.entity_id.toString() === this.fullyInfo.screensaverLightEntityId) {
524 | switch (event.data.service) {
525 | case 'turn_on':
526 | this.startScreensaver();
527 | break;
528 |
529 | case 'turn_off':
530 | this.stopScreensaver();
531 | break;
532 | }
533 |
534 | let brightness = event.data.service_data.brightness;
535 | if (brightness) {
536 | this.setScreenBrightness(brightness);
537 | }
538 | }
539 | }
540 | else if (this.fullyInfo.mediaPlayerEntityId && (event.data.domain === 'media_player')) {
541 | let targetEntityId;
542 | let serviceEntityId = event.data.service_data.entity_id;
543 |
544 | if (Array.isArray(serviceEntityId)) {
545 | targetEntityId = serviceEntityId.find(entityId => (entityId === this.fullyInfo.mediaPlayerEntityId));
546 | }
547 | else {
548 | targetEntityId = (serviceEntityId === this.fullyInfo.mediaPlayerEntityId) ? serviceEntityId : undefined;
549 | }
550 |
551 | if (targetEntityId) {
552 | switch (event.data.service) {
553 | case 'play_media':
554 | this.playMedia(event.data.service_data.media_content_id);
555 | break;
556 |
557 | case 'media_play':
558 | this.playMedia();
559 | break;
560 |
561 | case 'media_pause':
562 | case 'media_stop':
563 | this.pauseMedia();
564 | break;
565 |
566 | case 'volume_set':
567 | this.setVolume(event.data.service_data.volume_level);
568 | break;
569 |
570 | default:
571 | this.logWarning('FULLY_KIOSK', `Service not supported: ${event.data.service}`);
572 | break;
573 | }
574 | }
575 | }
576 |
577 | /*
578 | if ((event.data.domain === 'tts') && (event.data.service === 'google_say')) {
579 | if (this.fullyInfo.mediaPlayerEntityId === event.data.service_data.entity_id) {
580 | this.logDebug('FULLY_KIOSK', 'Playing TTS using Fully Kiosk');
581 | this.playTextToSpeech(event.data.service_data.message);
582 | }
583 | }
584 | */
585 | },
586 | 'call_service');
587 | }
588 |
589 | /***************************************************************************************************************************/
590 | /* Geolocation
591 | /***************************************************************************************************************************/
592 |
593 | updateCurrentPosition() {
594 | if (!navigator.geolocation) {
595 | return Promise.resolve(undefined);
596 | }
597 |
598 | return new Promise((resolve, reject) => {
599 | navigator.geolocation.getCurrentPosition(
600 | (position) => {
601 | this.logDebug('FULLY_KIOSK', `Current location: latitude: ${position.coords.latitude}, longitude: ${position.coords.longitude}`);
602 | this.position = position;
603 | resolve(position);
604 | },
605 | (err) => {
606 | this.logError('FULLY_KIOSK', 'Unable to retrieve location');
607 | reject(err);
608 | });
609 | })
610 | }
611 |
612 | /***************************************************************************************************************************/
613 | /* Errors / logging
614 | /***************************************************************************************************************************/
615 |
616 | handleError(message) {
617 | this.floorplan.handleError(message);
618 | }
619 |
620 | logError(area, message) {
621 | this.floorplan.logError(message);
622 | }
623 |
624 | logWarning(area, message) {
625 | this.floorplan.logWarning(area, message);
626 | }
627 |
628 | logInfo(area, message) {
629 | this.floorplan.logInfo(area, message);
630 | }
631 |
632 | logDebug(area, message) {
633 | this.floorplan.logDebug(area, message);
634 | }
635 |
636 | /***************************************************************************************************************************/
637 | /* Utility functions
638 | /***************************************************************************************************************************/
639 |
640 | debounce(func, wait, options) {
641 | let lastArgs,
642 | lastThis,
643 | maxWait,
644 | result,
645 | timerId,
646 | lastCallTime
647 |
648 | let lastInvokeTime = 0
649 | let leading = false
650 | let maxing = false
651 | let trailing = true
652 |
653 | if (typeof func != 'function') {
654 | throw new TypeError('Expected a function')
655 | }
656 | wait = +wait || 0
657 | if (options) {
658 | leading = !!options.leading
659 | maxing = 'maxWait' in options
660 | maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
661 | trailing = 'trailing' in options ? !!options.trailing : trailing
662 | }
663 |
664 | function invokeFunc(time) {
665 | const args = lastArgs
666 | const thisArg = lastThis
667 |
668 | lastArgs = lastThis = undefined
669 | lastInvokeTime = time
670 | result = func.apply(thisArg, args)
671 | return result
672 | }
673 |
674 | function leadingEdge(time) {
675 | // Reset any `maxWait` timer.
676 | lastInvokeTime = time
677 | // Start the timer for the trailing edge.
678 | timerId = setTimeout(timerExpired, wait)
679 | // Invoke the leading edge.
680 | return leading ? invokeFunc(time) : result
681 | }
682 |
683 | function remainingWait(time) {
684 | const timeSinceLastCall = time - lastCallTime
685 | const timeSinceLastInvoke = time - lastInvokeTime
686 | const timeWaiting = wait - timeSinceLastCall
687 |
688 | return maxing
689 | ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
690 | : timeWaiting
691 | }
692 |
693 | function shouldInvoke(time) {
694 | const timeSinceLastCall = time - lastCallTime
695 | const timeSinceLastInvoke = time - lastInvokeTime
696 |
697 | // Either this is the first call, activity has stopped and we're at the
698 | // trailing edge, the system time has gone backwards and we're treating
699 | // it as the trailing edge, or we've hit the `maxWait` limit.
700 | return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
701 | (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
702 | }
703 |
704 | function timerExpired() {
705 | const time = Date.now()
706 | if (shouldInvoke(time)) {
707 | return trailingEdge(time)
708 | }
709 | // Restart the timer.
710 | timerId = setTimeout(timerExpired, remainingWait(time))
711 | }
712 |
713 | function trailingEdge(time) {
714 | timerId = undefined
715 |
716 | // Only invoke if we have `lastArgs` which means `func` has been
717 | // debounced at least once.
718 | if (trailing && lastArgs) {
719 | return invokeFunc(time)
720 | }
721 | lastArgs = lastThis = undefined
722 | return result
723 | }
724 |
725 | function cancel() {
726 | if (timerId !== undefined) {
727 | clearTimeout(timerId)
728 | }
729 | lastInvokeTime = 0
730 | lastArgs = lastCallTime = lastThis = timerId = undefined
731 | }
732 |
733 | function flush() {
734 | return timerId === undefined ? result : trailingEdge(Date.now())
735 | }
736 |
737 | function pending() {
738 | return timerId !== undefined
739 | }
740 |
741 | function debounced(...args) {
742 | const time = Date.now()
743 | const isInvoking = shouldInvoke(time)
744 |
745 | lastArgs = args
746 | lastThis = this
747 | lastCallTime = time
748 |
749 | if (isInvoking) {
750 | if (timerId === undefined) {
751 | return leadingEdge(lastCallTime)
752 | }
753 | if (maxing) {
754 | // Handle invocations in a tight loop.
755 | timerId = setTimeout(timerExpired, wait)
756 | return invokeFunc(lastCallTime)
757 | }
758 | }
759 | if (timerId === undefined) {
760 | timerId = setTimeout(timerExpired, wait)
761 | }
762 | return result
763 | }
764 | debounced.cancel = cancel
765 | debounced.flush = flush
766 | debounced.pending = pending
767 | return debounced
768 | }
769 |
770 | throttle(func, wait, options) {
771 | let leading = true
772 | let trailing = true
773 |
774 | if (typeof func != 'function') {
775 | throw new TypeError('Expected a function');
776 | }
777 | if (options) {
778 | leading = 'leading' in options ? !!options.leading : leading
779 | trailing = 'trailing' in options ? !!options.trailing : trailing
780 | }
781 | return this.debounce(func, wait, {
782 | 'leading': leading,
783 | 'maxWait': wait,
784 | 'trailing': trailing
785 | })
786 | }
787 | }
788 |
789 | window.FullyKiosk = FullyKiosk;
790 | }).call(this);
791 |
--------------------------------------------------------------------------------
/www/custom_ui/floorplan/lib/jquery-3.3.1.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/
26 |
--------------------------------------------------------------------------------