diff --git a/.ameba.yml b/.ameba.yml
new file mode 100644
index 00000000000..3f439de24b7
--- /dev/null
+++ b/.ameba.yml
@@ -0,0 +1,57 @@
+Excluded:
+ - repositories/**/*.cr
+
+# NOTE: These should all be reviewed on an individual basis to see if their
+# complexity can be reasonably reduced.
+Metrics/CyclomaticComplexity:
+ Description: Disallows methods with a cyclomatic complexity higher than `MaxComplexity`
+ MaxComplexity: 10
+ Excluded:
+ - drivers/helvar/net.cr
+ - drivers/mulesoft/booking_api.cr
+ - drivers/samsung/displays/mdc_protocol.cr
+ - drivers/cisco/dna_spaces.cr
+ - drivers/cisco/meraki/dashboard.cr
+ - drivers/cisco/switch/snooping_catalyst.cr
+ - drivers/gantner/relaxx/protocol_json.cr
+ - drivers/place/bookings.cr
+ - drivers/place/area_management.cr
+ - drivers/place/smtp.cr
+ - drivers/hitachi/projector/cp_tw_series_basic.cr
+ - drivers/panasonic/projector/nt_control.cr
+ - drivers/lumens/dc193.cr
+ Enabled: false
+ Severity: Convention
+
+Lint/UselessAssign:
+ Description: Disallows useless variable assignments
+ # NOTE: Not enabled due to the extremely large hit count.
+ # Discussion with driver authors on whether this pattern is intended.
+ Enabled: false
+ Severity: Warning
+
+Style/VerboseBlock:
+ Description: Identifies usage of collapsible single expression blocks.
+ ExcludeCallsWithBlock: false
+ ExcludeMultipleLineBlocks: true
+ ExcludeOperators: false
+ ExcludePrefixOperators: false
+ ExcludeSetters: true
+ Enabled: false
+ Severity: Convention
+
+Style/VariableNames:
+ Description: Enforces variable names to be in underscored case
+ # NOTE: Not enabled due to the extremely large hit count.
+ # Discussion with driver authors on whether this pattern is intended.
+ Enabled: false
+ Severity: Convention
+
+# NOTE: These appear to be triggered by assignment in case expressions, could be an ameba bug
+Lint/ShadowingOuterLocalVar:
+ Description: Disallows the usage of the same name as outer local variables for block
+ or proc arguments.
+ Excluded:
+ - drivers/cisco/switch/snooping_catalyst.cr
+ Enabled: true
+ Severity: Warning
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000000..abad59c9f43
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,33 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: 'Bug: A concise description of the behaviour'
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+
+Steps to reproduce the behaviour or a minimal code snippet that demonstrates the behaviour.
+
+**Expected behaviour**
+
+A clear and concise description of what you expected to happen.
+
+**Screenshots or a paste of terminal output**
+
+If applicable, add screenshots to help explain your problem.
+
+**Versions (please complete the following information):**
+
+- Output of `$ crystal version`
+- Driver version [e.g. 3.x]
+
+**Additional context**
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/driver_migration.md b/.github/ISSUE_TEMPLATE/driver_migration.md
new file mode 100644
index 00000000000..bc50ed19207
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/driver_migration.md
@@ -0,0 +1,20 @@
+---
+name: Driver Migration
+about: Migrate existing Ruby Engine Driver to Crystal
+title: 'Driver Migration: Migrate existing Ruby driver'
+labels: driver
+assignees: ''
+
+---
+
+**Driver to be Migrated**
+
+Information about the driver to be migrated.
+
+**Link to Existing Driver**
+
+Link to existing Driver on Ruby Drivers Repo.
+
+**Additional context**
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/driver_request.md b/.github/ISSUE_TEMPLATE/driver_request.md
new file mode 100644
index 00000000000..b68b3c805a5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/driver_request.md
@@ -0,0 +1,32 @@
+---
+name: Driver Request
+about: Request a new driver to be created
+title: 'Driver Request: Information required to create a new driver'
+labels: driver
+assignees: ''
+
+---
+
+**Driver Type**
+
+Logic/Device/SSH/Websocket
+
+**Manufacturer**
+
+Manufacturer of device, software or service
+
+**Model/Service**
+
+Model or Service
+
+**Link to or Attach Device API or Protocol**
+
+If applicable, add screenshots to help explain your problem.
+
+**Describe any desired functionality**
+
+- Control all aspects of device
+
+**Additional context**
+
+Add any other context about the driver request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..01f460a18d6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,24 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: 'RFC: Concise description of desired feature'
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000000..7856c4b3ea4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,44 @@
+name: CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 6 * * 1"
+
+jobs:
+ style:
+ runs-on: ubuntu-latest
+ container:
+ image: crystallang/crystal
+ steps:
+ - uses: actions/checkout@v2
+ - name: Format
+ run: crystal tool format --check
+ - name: Lint
+ uses: crystal-ameba/github-action@v0.2.12
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ crystal:
+ - latest
+ - nightly
+ - 1.0.0
+ steps:
+ - uses: actions/checkout@v2
+ - name: Build drivers image
+ run: docker-compose build drivers
+ env:
+ CRYSTAL_VERSION: ${{ matrix.crystal }}
+ - name: Run docker-compose environment
+ run: docker-compose up -d
+ - name: Spec
+ run: docker exec placeos-drivers crystal spec -v --error-trace
+ - name: Driver Report
+ run: docker exec placeos-drivers /src/bin/report
diff --git a/.gitignore b/.gitignore
index 0792935e4a3..cb138427422 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,7 @@ lib
.shards
app
*.dwarf
+repositories/*
+bin
+.DS_Store
+*.rdb
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ffc7b6ac56d..00000000000
--- a/.travis.yml
+++ /dev/null
@@ -1 +0,0 @@
-language: crystal
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000000..c1424e57eb7
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,16 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug",
+ "type": "gdb",
+ "request": "launch",
+ "target": "./bin/test-harness",
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Compile",
+ "setupCommands": [
+ { "text": "-gdb-set follow-fork-mode child" }
+ ]
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000000..ce3aa5cfd9f
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,10 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Compile",
+ "command": "shards build --debug drivers",
+ "type": "shell"
+ }
+ ]
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000000..6434d0a6b73
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,34 @@
+ARG crystal_version=1.0.0
+FROM crystallang/crystal:${crystal_version}-alpine
+WORKDIR /src
+
+# Install the latest version of LibSSH2 and the GDB debugger
+RUN apk add --no-cache \
+ ca-certificates \
+ gdb \
+ iputils \
+ libssh2 libssh2-dev libssh2-static \
+ tzdata \
+ yaml-static
+
+# Add trusted CAs for communicating with external services
+RUN update-ca-certificates
+
+RUN mkdir -p /src/bin/drivers
+
+COPY shard.yml /src/shard.yml
+COPY shard.override.yml /src/shard.override.yml
+COPY shard.lock /src/shard.lock
+
+RUN shards install --production --ignore-crystal-version
+
+COPY src /src/src
+COPY spec /src/spec
+
+# Build App
+RUN shards build --error-trace --release --production --ignore-crystal-version
+
+# Run the app binding on port 8080
+EXPOSE 8080
+ENTRYPOINT ["/src/bin/test-harness"]
+CMD ["/src/bin/test-harness", "-b", "0.0.0.0", "-p", "8080"]
diff --git a/README.md b/README.md
index 1bd8efef9e8..7bff6a529d1 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,15 @@
-# Spider-Gazelle Application Template
+# PlaceOS Drivers
-[](https://travis-ci.org/spider-gazelle/spider-gazelle)
+[](https://github.com/PlaceOS/drivers/actions/workflows/ci.yml)
-Clone this repository to start building your own spider-gazelle based application
+Manage and test [PlaceOS](https://place.technology) drivers.
-## Documentation
+## Development
-* [Action Controller](https://github.com/spider-gazelle/action-controller) base class for building [Controllers](http://guides.rubyonrails.org/action_controller_overview.html)
-* [Active Model](https://github.com/spider-gazelle/active-model) base class for building [ORMs](https://en.wikipedia.org/wiki/Object-relational_mapping)
-* [Habitat](https://github.com/luckyframework/habitat) configuration and settings for Crystal projects
-* [router.cr](https://github.com/tbrand/router.cr) base request handling
-* [Radix](https://github.com/luislavena/radix) Radix Tree implementation for request routing
-* [HTTP::Server](https://crystal-lang.org/api/latest/HTTP/Server.html) built-in Crystal Lang HTTP server
- * Request
- * Response
- * Cookies
- * Headers
- * Params etc
+To spin up the test harness, clone the repository and run...
+```bash
+$ docker-compose up -d
+```
-Spider-Gazelle builds on the amazing performance of **router.cr** [here](https://github.com/tbrand/which_is_the_fastest).:rocket:
-
-
-## Testing
-
-`crystal spec`
-
-* to run in development mode `crystal ./src/app.cr`
-
-## Compiling
-
-`crystal build ./src/app.cr`
-
-### Deploying
-
-Once compiled you are left with a binary `./app`
-
-* for help `./app --help`
-* viewing routes `./app --routes`
-* run on a different port or host `./app -h 0.0.0.0 -p 80`
+Point a browser to [localhost:8085](http://localhost:8085), and you're good to go.
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000000..78b7cf8d757
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,35 @@
+version: "3.7"
+services:
+ redis:
+ image: eqalpha/keydb
+ restart: always
+ hostname: redis
+ environment:
+ - TZ=$TZ
+
+ drivers:
+ build:
+ context: .
+ args:
+ crystal_version: ${CRYSTAL_VERSION:-1.0.0}
+ image: placeos/drivers
+ restart: always
+ container_name: placeos-drivers
+ hostname: drivers
+ environment:
+ - CRYSTAL_PATH=lib:/lib/local-shards
+ depends_on:
+ - redis
+ ports:
+ - 127.0.0.1:8085:8080
+ - 127.0.0.1:4444:4444
+ volumes:
+ - ./drivers/:/src/drivers/
+ - ./repositories/:/src/repositories/
+ - ./lib/:/lib/local-shards/
+ - ./src/:/src/src
+ - ./spec/:/src/spec
+ - ./.git:/src/.git
+ environment:
+ - REDIS_URL=redis://redis:6379
+ - TZ=$TZ
diff --git a/docs/directory_structure.md b/docs/directory_structure.md
new file mode 100644
index 00000000000..88355f1f49b
--- /dev/null
+++ b/docs/directory_structure.md
@@ -0,0 +1,20 @@
+# Directory Structures
+
+PlaceOS core / drivers makes the assumption that the working directory one level
+up from the scratch directory. An example deployment structure:
+
+* Working dir: `/home/placeos/core`
+* Executable: `/home/placeos/core/bin/core`
+* Driver repositories: `/home/placeos/repositories`
+ * PlaceOS Drivers: `/home/placeos/repositories/drivers`
+* Driver executables: `/home/placeos/core/bin/drivers`
+ * Samsung driver: `/home/placeos/core/bin/drivers/353b53_samsung_display_md_series_cr`
+
+However when developing the structure will look more like:
+
+* Working dir: `/home/steve/drivers`
+* Driver repository: `/home/steve/drivers`
+* Driver executables: `/home/steve/drivers/bin/drivers`
+ * Samsung driver: `/home/placeos/core/bin/drivers/353b53_samsung_display_md_series_cr`
+
+The primary difference between production and development is PlaceOS core, in production, will be cloning repositories and installing shards as required.
diff --git a/docs/gdb-entitlement.xml b/docs/gdb-entitlement.xml
new file mode 100644
index 00000000000..9d9251f55d9
--- /dev/null
+++ b/docs/gdb-entitlement.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.cs.debugger
+
+
+
+
+
diff --git a/docs/guide-event-emails.md b/docs/guide-event-emails.md
new file mode 100644
index 00000000000..cd3d40d286d
--- /dev/null
+++ b/docs/guide-event-emails.md
@@ -0,0 +1,442 @@
+# How to email people when an event occurs
+
+There are three aspects to this
+
+1. real-time send an email as soon as an event occurs
+2. batching events (either periodically or via a CRON)
+3. managing state (state machine management)
+
+i.e. send email straight away if the event is today otherwise send them at 7am every morning and mark emails as sent.
+Poll every 15min to send any emails that were missed due to an outage. (by checking state)
+
+
+## Example logic driver
+
+```crystal
+require "placeos-driver/interface/mailer"
+
+class DeskBookingNotification < PlaceOS::Driver
+ descriptive_name "Desk Booking Approval"
+ generic_name :BookingApproval
+
+ default_settings({
+ # https://www.iana.org/time-zones
+ timezone: "Australia/Sydney",
+ # https://crystal-lang.org/api/latest/Time/Format.html
+ date_time_format: "%c",
+ time_format: "%l:%M%p",
+ date_format: "%A, %-d %B",
+ booking_type: "desk",
+ buildings: ["zone-123", "zone-456"],
+ })
+
+ # this ensures these variables are not nilable
+ @time_zone : Time::Location = Time::Location.load("Australia/Sydney")
+ @date_time_format : String = "%c"
+ @time_format : String = "%l:%M%p"
+ @date_format : String = "%A, %-d %B"
+ @booking_type : String = "desk"
+ @buildings : Array(String) = [] of String
+
+ def on_update
+ # Update the instance variables based on the settings
+ time_zone = setting?(String, :calendar_time_zone).presence || "Australia/Sydney"
+ @time_zone = Time::Location.load(time_zone)
+ @date_time_format = setting?(String, :date_time_format) || "%c"
+ @time_format = setting?(String, :time_format) || "%l:%M%p"
+ @date_format = setting?(String, :date_format) || "%A, %-d %B"
+ @booking_type = setting?(String, :booking_type).presence || "desk"
+ @buildings = setting?(Array(String), :buildings) || [] of String
+
+ # configure any schedules here
+ # https://github.com/spider-gazelle/tasker
+ schedule.clear
+ schedule.every(5.minutes) { poll_bookings }
+ schedule.cron("30 7 * * *", @time_zone) { poll_bookings }
+ end
+
+ def on_load
+ # Some form of asset booking has occurred (such as a desk booking)
+ monitor("staff/booking/changed") { |_subscription, payload| check_booking(payload) }
+
+ on_update
+ end
+
+ # Get a reference to a module that can be used to send emails
+ def mailer
+ system.implementing(Interface::Mailer)
+ end
+
+ # Access another module in the system
+ accessor staff_api : StaffAPI_1
+
+ protected def check_booking(payload : String)
+ logger.debug { "received booking event payload: #{payload}" }
+ booking_details = Booking.from_json payload
+ process_booking(booking_details)
+ end
+
+ # ensure we don't have two fibers processing this at once
+ # (technically the driver is thread safe, but it is concurrent)
+ @check_bookings_mutex = Mutex.new
+
+ @[Security(Level::Support)]
+ def poll_bookings(months_from_now : Int32 = 2)
+ # Clean up old debounce data
+ expired = 5.minutes.ago.to_unix
+ @debounce.reject! { |_, (_event, entered)| expired > entered }
+
+ now = Time.utc.to_unix
+ later = months_from_now.months.from_now.to_unix
+
+ @check_bookings_mutex.synchronize do
+ @buildings.each do |building_zone|
+ # bookings that haven't been approved
+ bookings = staff_api.query_bookings(
+ type: @booking_type,
+ period_start: now,
+ period_end: later,
+ zones: [building_zone],
+ approved: false,
+ rejected: false,
+ created_before: 2.minutes.ago.to_unix
+ ).get.as_a
+
+ # bookings that have been approved
+ bookings = bookings + staff_api.query_bookings(
+ type: @booking_type,
+ period_start: now,
+ period_end: later,
+ zones: [building_zone],
+ approved: true,
+ rejected: false,
+ created_before: 2.minutes.ago.to_unix
+ ).get.as_a
+
+ # Convert to nice objects
+ bookings = Array(Booking).from_json(bookings.to_json)
+
+ logger.debug { "checking #{bookings.size} requested bookings in #{building_zone}" }
+ bookings.each { |booking_details| process_booking(booking_details) }
+ end
+ end
+ end
+
+ # Booking id => event action, timestamp
+ @debounce = {} of Int64 => {String?, Int64}
+ @bookings_checked = 0_u64
+
+ # See the booking model at the end of this document
+ protected def process_booking(booking_details : Booking)
+ # Ignore when a bookings state is updated
+ return if {"process_state", "metadata_changed"}.includes?(booking_details.action)
+
+ # Ignore the same event in a short period of time
+ previous = @debounce[booking_details.id]?
+ return if previous && previous[0] == booking_details.action
+ @debounce[booking_details.id] = {booking_details.action, Time.utc.to_unix}
+
+ # timezone, if different from the default
+ timezone = booking_details.timezone.presence || @time_zone.name
+ location = Time::Location.load(timezone)
+
+ # https://crystal-lang.org/api/0.35.1/Time/Format.html
+ # date and time (Tue Apr 5 10:26:19 2016)
+ starting = Time.unix(booking_details.booking_start).in(location)
+ ending = Time.unix(booking_details.booking_end).in(location)
+
+ # Ignore changes to meetings that have already ended
+ return if Time.utc > ending
+
+ building_zone, building_name = get_building_details(booking_details.zones)
+
+ # These are the available keys for use in the templates
+ args = {
+ booking_id: booking_details.id,
+ start_time: starting.to_s(@time_format),
+ start_date: starting.to_s(@date_format),
+ start_datetime: starting.to_s(@date_time_format),
+ end_time: ending.to_s(@time_format),
+ end_date: ending.to_s(@date_format),
+ end_datetime: ending.to_s(@date_time_format),
+ starting_unix: booking_details.booking_start,
+
+ desk_id: booking_details.asset_id,
+ user_id: booking_details.user_id,
+ user_email: booking_details.user_email,
+ user_name: booking_details.user_name,
+ reason: booking_details.title,
+
+ level_zone: booking_details.zones.reject { |z| z == building_zone }.first?,
+ building_zone: building_zone,
+ building_name: building_name,
+ support_email: support_email,
+
+ approver_name: booking_details.approver_name,
+ approver_email: booking_details.approver_email,
+
+ booked_by_name: booking_details.booked_by_name,
+ booked_by_email: booking_details.booked_by_email,
+ }
+
+ case booking_details.action
+ when "create", "changed"
+ # check if email already sent and we can ignore this one
+ next if booking_details.process_state == "notification_sent"
+
+ mailer.send_template(
+ to: booking_details.user_email,
+ template: {"bookings", "booking_notification"},
+ args: args
+ )
+
+ # update the booking state (if there are multiple states a booking can be in)
+ staff_api.booking_state(booking_details.id, "notification_sent").get
+ when "approved"
+ # if there is an approval process
+ mailer.send_template(
+ to: booking_details.user_email,
+ template: {"bookings", "booking_approved"},
+ args: args
+ )
+
+ staff_api.booking_state(booking_details.id, "approval_sent").get
+ when "rejected", "checked_in"
+ mailer.send_template(
+ to: booking_details.user_email,
+ template: {"bookings", booking_details.action},
+ args: args
+ )
+ when "cancelled"
+ # maybe someone else cancelled your booking and you have a custom template for that
+ third_party = booking_details.approver_email && booking_details.approver_email != booking_details.user_email.downcase
+
+ mailer.send_template(
+ to: booking_details.user_email,
+ template: {"bookings", third_party ? "cancelled_by" : "cancelled"},
+ args: args
+ )
+
+ # maybe you want to notifty the persons manager about this
+ if manager_email = get_manager(user_email).try(&.at(0))
+ mailer.send_template(
+ to: manager_email,
+ template: {"bookings", "manager_notify_cancelled"},
+ args: args
+ )
+ end
+ end
+
+ # nice to see some status in backoffice
+ @bookings_checked += 1
+ self[:bookings_checked] = @bookings_checked
+ end
+
+ # id => tags, name
+ @zone_cache = {} of String => Tuple(Array(String), String)
+
+ def get_building_details(zones : Array(String))
+ zones.each do |zone_id|
+ zone_info = @zone_cache[zone_id]? || get_zone(zone_id)
+ next unless zone_info
+ next unless zone_info[0].includes?("building")
+
+ return {zone_id, zone_info[1]}
+ end
+
+ nil
+ end
+
+ def get_zone(zone_id : String)
+ zone = staff_api.zone(zone_id).get
+ tags = zone["tags"].as_a.map(&.as_s)
+ name = zone["name"].as_s
+ tuple = {tags, name}
+ @zone_cache[zone_id] = tuple
+ tuple
+ rescue error
+ logger.warn(exception: error) { "error obtaining zone details for #{zone_id}" }
+ nil
+ end
+
+ @[Security(Level::Support)]
+ def get_manager(staff_email : String)
+ # The Calendar driver is hooked up to MS Graph API for example
+ # could have used an accessor here like `staff_api`, that's optional
+ manager = system[:Calendar_1].get_user_manager(staff_email).get
+ {(manager["email"]? || manager["username"]).as_s, manager["name"].as_s}
+ rescue error
+ logger.warn(exception: error) { "failed to obtain manager of #{staff_email}" }
+ {nil, nil}
+ end
+end
+
+```
+
+
+### List of Staff API events
+
+These are events that can be monitored `monitor("event/path") { |sub, payload| }`
+
+* booking (desk, car space etc) - `"staff/booking/changed"`
+ * [boooking event model](https://github.com/place-labs/staff-api/blob/master/src/controllers/bookings.cr#L80)
+ * `action` types: create, cancelled, changed, metadata_changed, approved, rejected, checked_in, process_state
+* events (calendar events) - `"staff/event/changed"`
+ * [event event model](https://github.com/place-labs/staff-api/blob/master/src/controllers/events.cr#L130)
+ * `action` types: create, update, cancelled
+* a guest has been invited onsite - `"staff/guest/attending"`
+ * [guest attending model](https://github.com/place-labs/staff-api/blob/master/src/controllers/events.cr#L195)
+ * `action` types: meeting_created, meeting_update
+* a guest has arrived onsite - `"staff/guest/checkin"`
+ * [guest checkin model](https://github.com/place-labs/staff-api/blob/master/src/controllers/events.cr#L723)
+
+
+### Booking Model
+
+This model covers events and API responses
+
+```crystal
+
+class Booking
+ include JSON::Serializable
+
+ # This is to support events
+ property action : String?
+
+ property id : Int64
+ property booking_type : String
+ property booking_start : Int64
+ property booking_end : Int64
+ property timezone : String?
+
+ # events use resource_id instead of asset_id
+ property asset_id : String?
+ property resource_id : String?
+
+ def asset_id : String
+ (@asset_id || @resource_id).not_nil!
+ end
+
+ property user_id : String
+ property user_email : String
+ property user_name : String
+
+ property zones : Array(String)
+
+ property checked_in : Bool?
+ property rejected : Bool?
+ property approved : Bool?
+ property process_state : String?
+ property last_changed : Int64?
+
+ property approver_name : String?
+ property approver_email : String?
+
+ property booked_by_name : String
+ property booked_by_email : String
+
+ property checked_in : Bool?
+ property title : String?
+ property description : String?
+
+ property extension_data : Hash(String, JSON::Any)
+
+ def in_progress?
+ now = Time.utc.to_unix
+ now >= @booking_start && now < @booking_end
+ end
+
+ def changed
+ Time.unix(last_changed.not_nil!)
+ end
+end
+
+```
+
+### Email templates
+
+Email templates are applied to the mailer driver and then other drivers can use them to send emails.
+
+see the [mailer interface](https://github.com/PlaceOS/driver/blob/master/src/placeos-driver/interface/mailer.cr#L27) for details on available params
+
+The templates are settings, structured like:
+
+```yaml
+
+email_templates:
+ category:
+ template_name:
+ subject: the email subject line with %{variables}
+ text: the text version of an email
+ html:
the HTML version of the email
+
+```
+
+typically only the `html` version of an email is required
+
+```yaml
+
+email_templates:
+ bookings:
+ rejected:
+ subject: 'Desk Booking: Manager rejection'
+ html: >
+
+
+ This is a short note to advise that your desk booking request for
+ %{start_date} at %{building_name} has been rejected.
+
+
+
+ Please reach out to your manager %{approver_name} if you would like
+ to follow up.
+
+
+
+ Your request has been removed from the system and we look forward to
+ welcoming you to our workplace in the future.
+
+
+
+ Kind Regards
+
+
+
+ The Corporate Real Estate Team
+
+
+ cancelled:
+ subject: Desk booking cancellation confirmation
+ text: >
+ Thank you for taking the time to cancel your booking which we appreciate
+ so we can continue to operate with efficiency and excellence.
+
+
+ Your desk booking on %{start_date} at %{building_name} has been
+ cancelled.
+
+
+ Please reach out to your workplace support team should you have any
+ other queries, otherwise we look forward to seeing you soon
+ html: >
+
+
+ Thank you for taking the time to cancel your booking which we appreciate
+ so we can continue to operate with efficiency and excellence.
+
+
+
+ Your desk booking on %{start_date} at %{building_name} has been
+ cancelled.
+
+
+
+ Please reach out to your workplace support team should you have any other queries,
+ otherwise we look forward to seeing you soon
+
+
+
+```
diff --git a/docs/http-api.md b/docs/http-api.md
new file mode 100644
index 00000000000..9e908404b81
--- /dev/null
+++ b/docs/http-api.md
@@ -0,0 +1,154 @@
+# HTTP API
+
+Primarily for development.
+
+
+## GET /build
+
+Returns the list of available drivers
+
+* `repository=folder_name` (optional) if you wish to specify a third party repository
+* `compiled=true` (optional) if you only want the list of compiled drivers
+
+```json
+
+["drivers/place/spec_helper.cr", "..."]
+```
+
+
+### GET /build/repositories
+
+Returns the list of 3rd party repositories
+
+```json
+
+["private_drivers", "..."]
+```
+
+
+### GET /build/repository_commits
+
+Returns the list of available commits at the repository level
+
+* `repository=folder_name` (optional) if you wish to specify a third party repository
+* `count=50` (optional) if you want more or less commits
+
+```json
+
+{
+ "commit": "01519d6",
+ "date": "2019-06-02T23:59:22+10:00",
+ "author": "Stephen von Takach",
+ "subject": "implement websocket spec runner"
+}
+```
+
+
+### GET /build/{{escaped driver path}}
+
+Returns the list of compiled versions of the specified file are available
+
+```json
+
+["private_drivers_cr_01519d6", "..."]
+```
+
+
+### GET /build/{{escaped driver path}}/commits
+
+Returns the list of available commits for the current driver
+
+* `repository=folder_name` (optional) if you wish to specify a third party repository
+* `count=50` (optional) if you want more or less commits
+
+```json
+
+{
+ "commit": "01519d6",
+ "date": "2019-06-02T23:59:22+10:00",
+ "author": "Stephen von Takach",
+ "subject": "implement websocket spec runner"
+}
+```
+
+
+### POST /build
+
+compiles a driver
+
+* `driver=drivers/path.cr` (required) the path to the driver
+* `commit=01519d6` (optional) defaults to head
+
+
+### DELETE /build/{{escaped driver path}}
+
+deletes compiled versions of a driver
+
+* `repository=folder_name` (optional) if you wish to specify a third party repository
+* `commit=01519d6` (optional) deletes all versions of a driver if not specified
+
+
+## GET /test
+
+Lists the available specs
+
+```json
+
+["drivers/place/spec_helper_spec.cr", "..."]
+```
+
+
+### GET /test/{{escaped spec path}}/commits
+
+Returns the list of available commits for the specified spec
+
+* `repository=folder_name` (optional) if you wish to specify a third party repository
+* `count=50` (optional) if you want more or less commits
+
+```json
+
+{
+ "commit": "01519d6",
+ "date": "2019-06-02T23:59:22+10:00",
+ "author": "Stephen von Takach",
+ "subject": "implement websocket spec runner"
+}
+```
+
+
+### POST /test
+
+Compiles and runs a spec and returns the output
+
+* `repository=folder_name` (optional) if you wish to specify a third party repository
+* `driver=drivers/path/to/file.cr` (required) the driver you want to test
+* `spec=drivers/path/to/file_spec.cr` (required) the spec you want to run on the driver
+* `commit=01519d6` (optional) the commit you would like the driver to be running at
+* `spec_commit=01519d6` (optional) the commit you would like the spec to be running at
+* `force=true` (optional) forces a re-compilation of the driver and spec
+* `debug=true` (optional) compiles the files with debugging symbols
+
+```text
+Launching spec runner
+Launching driver: /Users/steve/Documents/projects/placeos/drivers/bin/drivers/drivers_place_private_helper_cr_4f6e0cd
+... starting driver IO services
+... starting module
+... waiting for module
+... module connected
+... enabling debug output
+... starting spec
+... spec complete
+... terminating driver gracefully
+Driver terminated with: 0
+
+
+Finished in 15.65 milliseconds
+0 examples, 0 failures, 0 errors, 0 pending
+
+spec runner exited with 0
+```
+
+
+### WebSocket /test/run_spec
+
+Same requirements as `POST /test` above however it streams the response
diff --git a/docs/runtime-debugging.md b/docs/runtime-debugging.md
new file mode 100644
index 00000000000..9fe51783978
--- /dev/null
+++ b/docs/runtime-debugging.md
@@ -0,0 +1,195 @@
+# Runtime Debugging
+
+This is supported via VS Code on OSX or Linux platforms.
+It might be possible to do remote debugging on Windows in conjunction with the Linux Layer.
+
+* Requires [VS Code](https://code.visualstudio.com/)
+ * install [Crystal Lang](https://marketplace.visualstudio.com/items?itemName=faustinoaq.crystal-lang) extension
+ * install [Native Debug](https://marketplace.visualstudio.com/items?itemName=webfreak.debug) extension
+* Requires [GDB](https://www.gnu.org/software/gdb/)
+ * On OSX install using [Homebrew](https://brew.sh/)
+ * Then code sign the executable: https://sourceware.org/gdb/wiki/PermissionsDarwin
+ * The `gdb-entitlement.xml` file is in this folder
+ * When creating the signing certificate follow [this guide](https://apple.stackexchange.com/questions/309017/unknown-error-2-147-414-007-on-creating-certificate-with-certificate-assist)
+
+This should also work with [LLDB](https://lldb.llvm.org/) on OSX however [has issues](https://github.com/crystal-lang/crystal/issues/4457).
+
+
+## Debug on VSCode
+
+By convention the project directory name is the same as your application name, if you have changed it, please update `${workspaceFolderBasename}` with the name configured inside `shards.yml`
+
+### 1. `tasks.json` configuration to compile a crystal project
+
+```javascript
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Compile",
+ "command": "shards build --debug ${workspaceFolderBasename}",
+ "type": "shell"
+ }
+ ]
+}
+```
+
+### 2. `launch.json` configuration to debug a binary
+
+#### Using GDB
+
+```javascript
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug",
+ "type": "gdb",
+ "request": "launch",
+ "target": "./bin/${workspaceFolderBasename}",
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Compile"
+ }
+ ]
+}
+```
+
+#### Using LLDB
+
+```javascript
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Debug",
+ "type": "lldb-mi",
+ "request": "launch",
+ "target": "./bin/${workspaceFolderBasename}",
+ "cwd": "${workspaceRoot}",
+ "preLaunchTask": "Compile"
+ }
+ ]
+}
+```
+
+### 3. Then hit the DEBUG green play button
+
+
+
+## Tips and Tricks for debugging Crystal applications
+
+### 1. Use debugger keyword
+
+Instead of putting breakpoints using commands inside GDB or LLDB you can try to set a breakpoint using `debugger` keyword.
+
+```ruby
+i = 0
+while i < 3
+ i += 1
+ debugger # => breakpoint
+end
+```
+
+### 2. Avoid breakpoints inside blocks
+
+Currently, Crystal lacks support for debugging inside of blocks. If you put a breakpoint inside a block, it will be ignored.
+
+As a workaround, use `pp` to pretty print objects inside of blocks.
+
+```ruby
+3.times do |i|
+ pp i
+end
+# i => 1
+# i => 2
+# i => 3
+```
+
+### 3. Try `@[NoInline]` to debug arguments data
+
+Sometimes crystal will optimize argument data, so the debugger will show `` instead of the arguments. To avoid this behavior use the `@[NoInline]` attribute before your function implementation.
+
+```ruby
+@[NoInline]
+def foo(bar)
+ debugger
+end
+```
+
+### 4. Printing strings objects \(GDB\)
+
+To print string objects in the debugger:
+
+First, setup the debugger with the `debugger` statement:
+
+```ruby
+foo = "Hello World!"
+debugger
+```
+
+Then use `print` in the debugging console.
+
+```bash
+(gdb) print &foo.c
+$1 = (UInt8 *) 0x10008e6c4 "Hello World!"
+```
+
+Or add `&foo.c` using a new variable entry on watch section in VSCode debugger
+
+
+
+### 5. Printing array variables
+
+To print array items in the debugger:
+
+First, setup the debugger with the `debugger` statement:
+
+```ruby
+foo = ["item 0", "item 1", "item 2"]
+debugger
+```
+
+Then use `print` in the debugging console:
+
+```bash
+(gdb) print &foo.buffer[0].c
+$19 = (UInt8 *) 0x10008e7f4 "item 0"
+```
+
+Change the buffer index for each item you want to print.
+
+### 6. Printing instance variables
+
+For printing `@foo` var in this code:
+
+```ruby
+class Bar
+ @foo = 0
+ def baz
+ debugger
+ end
+end
+
+Bar.new
+```
+
+You can use `self.foo` in the debugger terminal or VSCode GUI.
+
+### 7. Print hidden objects
+
+Some objects do not show at all. You can unhide them using the `.to_s` method and a temporary debugging variable, like this:
+
+```ruby
+def bar(hello)
+ "#{hello} World!"
+end
+
+def foo(hello)
+ bar_hello_to_s = bar(hello).to_s
+ debugger
+end
+
+foo("Hello")
+```
+
+This trick allows showing the `bar_hello_to_s` variable inside the debugger tool.
diff --git a/docs/setup.md b/docs/setup.md
new file mode 100644
index 00000000000..7c6cd358d15
--- /dev/null
+++ b/docs/setup.md
@@ -0,0 +1,37 @@
+# Setup
+
+This allows you to build and test drivers without installing or running the complete PlaceOS service.
+
+1. clone the drivers repository: `git clone https://github.com/placeos/drivers drivers`
+2. clone private repositories here: `mkdir ./drivers/repositories`
+
+
+## OSX
+
+Install [Homebrew](https://brew.sh/) to install dependencies
+
+* Install [Crystal Lang](https://crystal-lang.org/reference/installation/): `brew install crystal`
+* Install libssh2: `brew install libssh2`
+* Install redis: `brew install redis`
+
+Ensure the following lines are in your `.bashrc` file
+
+```shell
+export PATH="/usr/local/opt/llvm/bin:$PATH"
+export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/opt/openssl/lib/pkgconfig
+```
+
+
+## Running Specs
+
+1. Ensure redis is running: `redis-server`
+2. Install dependencies: `cd drivers; shards update`
+3. Launch application: `crystal run ./src/app.cr`
+4. Browse to: http://localhost:3000/
+
+Now you can build drivers and run specs:
+
+* Build a drvier or spec: `curl -X POST "http://localhost:3000/build?driver=drivers/helvar/net.cr"`
+* Run a spec: `curl -X POST "http://localhost:3000/test?driver=drivers/lutron/lighting.cr&spec=drivers/lutron/lighting_spec.cr"`
+
+To build or test against drivers in private repositories include the repository param: `repository=private_drivers`
diff --git a/docs/writing-a-driver.md b/docs/writing-a-driver.md
new file mode 100644
index 00000000000..67247581c4a
--- /dev/null
+++ b/docs/writing-a-driver.md
@@ -0,0 +1,490 @@
+# How to write a driver
+
+There are three kind of drivers
+
+* Streaming IO (TCP, SSH, UDP, Multicast ect)
+* HTTP Client
+* Logic
+
+From a driver structure standpoint there is no difference between these types.
+
+* The same driver can be used over a TCP, UDP or SSH transport.
+* All drivers support HTTP methods if a URI endpoint is defined.
+* If a driver is associated with a System then it has access to logic helpers
+
+However typically a driver will only implement one of these interfaces.
+
+
+## Concepts
+
+Backing a driver is few different pieces that make it function.
+
+* Queue
+* Transport
+* Subscriptions
+* Scheduler
+* Settings
+* Logger
+* Metadata
+* Security
+* Interfaces
+
+
+### Queue
+
+The queue is a list of potentially asynchronous tasks that should be performed in a sequence.
+
+* Each task has a priority (defaults to `50`) - higher priority tasks run first
+* Tasks can be named. If a new task is added with the same name it replaces the existing task.
+* Tasks have a timeout (defaults to `5.seconds`)
+* Tasks can be retried (defaults to `3` before failing)
+
+Tasks have a callback that is used to run the task
+
+```crystal
+
+# => you can set queue defaults globally
+
+# set a delay between the current task completing and the next task
+queue.delay = 1.second
+queue.retries = 5
+
+queue(priority: 20, timeout: 1.second) do |task|
+ # perform action here
+
+ # signal result
+ task.success("optional success value")
+ task.abort("optional failure message")
+ task.retry
+
+ # Give me more time to complete the task
+ task.reset_timers
+end
+
+```
+
+In most cases you won't need to use the queue explicitly however it is good to understand that it is there and how it functions.
+
+
+### Transport
+
+The transport loaded is defined by settings in the database.
+
+#### Streaming IO
+
+You should always tokenise your streams.
+This can be handled automatically by the [built in tokeniser](https://github.com/spider-gazelle/tokenizer)
+
+```crystal
+
+def on_load
+ transport.tokenizer = Tokenizer.new("\r\n")
+end
+
+```
+
+There are a few ways to use streaming IO methods:
+
+1. send and receive
+
+```crystal
+
+def perform_action
+ # You call send with some data.
+ # you can also optionally pass some queue options to the function
+ send("message data", priority: 30, name: "generic-message")
+end
+
+# A common received function for handling responses
+def received(data, task)
+ # data is always `Bytes`
+ # task is always `PlaceOS::Driver::Task?` (i.e. could be nil if no active task)
+
+ # convert data into the appropriate format
+ data = String.new(data)
+
+ # decide if the request was a success or not
+ # you can pass any value that is JSON serialisable to success
+ # (if it can't be serialised then nil is sent)
+ task.try &.success(data)
+end
+
+```
+
+2. send and callback
+
+```crystal
+
+def perform_action
+ request = "build request"
+
+ send(request, priority: 30, name: "generic-message") do |data, task|
+ data = String.new(data)
+
+ # process response here (might need to know the request context)
+
+ task.try &.success(data)
+ end
+end
+
+```
+
+3. send immediately (no queuing)
+
+```crystal
+
+def perform_action_now!
+ transport.send("no queue")
+end
+
+```
+
+You can also add a pre-processor to data coming in. This can be useful
+if you want to strip away a protocol layer i.e. you are communicating
+over Telnet and want to remove the telnet signals leaving the raw
+comms for tokenising
+
+```crystal
+
+def on_load
+ transport.pre_processor do |bytes|
+ # you must return some byte data or nil if no processing is required
+ # tokenisation occurs on the data returned here
+ bytes[1..-2]
+ end
+end
+
+def received(data, task)
+ # data coming in here is both pre_processed and tokenised
+end
+
+```
+
+
+#### HTTP Client
+
+All drivers have built in methods for performing HTTP requests.
+
+* For streaming IO devices this defaults to `http://device.ip.address` or `https` if the transport is using TLS / SSH.
+* All devices can provide a custom HTTP base URI.
+
+There are methods for all the typical HTTP verbs: get, post, put, patch, delete
+
+```crystal
+
+def perform_action
+ basic_auth = "Basic #{Base64.strict_encode("#{@username}:#{@password}")}"
+
+ response = post("/v1/message/path", body: {
+ messages: numbers,
+ }.to_json, headers: {
+ "Authorization" => basic_auth,
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ }, params: {
+ "key" => "value"
+ })
+
+ raise "request failed with #{response.status_code}" unless (200...300).include?(data.status_code)
+end
+
+```
+
+
+#### Special SSH methods
+
+SSH connections will attempt to open a shell to the remote device however sometimes you may be able to execute operations independently.
+
+```crystal
+
+def perform_action
+ # if the application launched supports input you can use the bidirectional IO
+ # to communicate with the app
+ io = exec("command")
+end
+
+```
+
+
+#### Logic drivers
+
+The main difference between logic drivers and other transports is that a logic module is directly associated with a System and cannot be shared. (all other drivers can appear in multiple systems)
+
+* You can access remote modules in the system via the `system` helper
+
+```crystal
+
+# Get a system proxy
+sys = system
+sys.name #=> "Name of system"
+sys.email #=> "resource@email.address"
+sys.capacity #=> 12
+sys.bookable #=> true
+sys.id #=> "sys-tem~id"
+sys.modules #=> ["Array", "Of", "Unique", "Module", "Names", "In", "System"]
+sys.count("Module") #=> 3
+sys.implementing(PlaceOS::Driver::Interface::Powerable) #=> ["Camera", "Display"]
+
+# Look at status on a remote module
+system[:Display][:power] #=> true
+
+# Access a different module index
+system[:Display_2][:power]
+system.get(:Display, 2)[:power]
+
+# Access all modules of a type
+system.all(:Display)
+
+# Check if a module exists
+system.exists?(:Display) #=> true
+system.exists?(:Display_2) #=> false
+
+```
+
+you can bind to state in remote modules
+
+```crystal
+
+bind Display_1, :power, :power_changed
+
+private def power_changed(subscription, new_value)
+ logger.debug new_value
+end
+
+
+# you can also bind to internal state (available in all drivers)
+bind :power, :power_changed
+
+```
+
+It's also possible to create shortcuts to other modules.
+This is powerful as these shortcuts are exposed as metadata - allowing backoffice to perform system verification.
+
+For example, consider the following video conference system:
+
+```crystal
+
+# It requires at least one camera that can move and be turned on and off
+accessor camera : Array(Camera), implementing: [Powerable, Moveable]
+
+# Optional room blinds that can be opened and closed
+accessor blinds : Array(Blind)?, implementing: [Switchable]
+
+# A single display is required with an optional screen (maybe it's a projector)
+accessor main_display : Display_1, implementing: Powerable
+accessor screen : Screen?
+
+```
+
+Cross system communication is possible if you know the ID of the remote system.
+
+```crystal
+# once you have reference to the remote system you can perform any
+# actions that you might perform on the local system
+sys = system("sys-12345")
+
+sys.name #=> "Name of remote system"
+sys[:Display_2][:power] #=> true
+```
+
+
+### Subscriptions
+
+You can dynamically bind to state of interest in remote modules
+
+```crystal
+
+# subscription is returned and provided with every status update in the callback
+subscription = system.subscribe(:Display_1, :power) do |subscription, new_value|
+ # values are always raw JSON strings
+ JSON.parse(new_value)
+end
+
+# Local subscriptions
+subscription = subscribe(:state) do |subscription, new_value|
+ # values are always raw JSON strings
+ JSON.parse(new_value)
+end
+
+# Clearing all subscriptions
+subscriptions.clear
+
+```
+
+Similarly to subscriptions, there are channels that can be setup for broadcasting
+arbitrary data that might not need be exposed as state.
+
+```crystal
+
+subscription = monitor(:channel_name) do |subscription, new_value|
+ # values are always raw JSON strings
+ JSON.parse(new_value)
+end
+
+# Publish something on the channel to all listeners
+publish(:channel_name, "some event")
+
+```
+
+
+### Scheduler
+
+There is a built in scheduler: https://github.com/spider-gazelle/tasker
+
+```crystal
+
+def connected
+ schedule.every(40.seconds) { poll_device }
+ schedule.in(200.milliseconds) { send_hello }
+end
+
+def disconnected
+ schedule.clear
+end
+
+```
+
+
+### Settings
+
+Settings are stored as JSON and then extracted as required, serialising to the specified type
+There are two types:
+
+* Required settings - raise an error if the setting is unavailable
+* Optional settings - return `nil` if the setting is unavailable
+
+NOTE:: All settings will raise an error if they exist but fail to serialise (as they are not formatted correctly etc)
+
+```crystal
+
+# Required settings
+def on_update
+ @display_id = setting(Int32, :display_id)
+
+ # Can extract deeply nested values
+ # i.e. {input: {list: ["HDMI", "VGA"] }}
+ @primary_input = setting(InputEnum, :input, :list, 0)
+end
+
+# Optional settings (you can optionally provide a default)
+def on_update
+ @display_id = setting?(Int32, :display_id) || 1
+ @primary_input = setting?(InputEnum, :input, :list, 0) || InputEnum::HDMI
+end
+
+```
+
+You can update the local settings of a module, persisting them to the database. Settings must be JSON serialisable
+
+```crystal
+define_setting(:my_setting_name, "some JSON serialisable data")
+```
+
+
+### Logger
+
+There is a logger available: https://crystal-lang.org/api/latest/Logger.html
+
+* Warning and above are written to disk.
+* debug and info are only available when there is an open debugging session.
+
+```crystal
+
+logger.warn { "error unknown response" }
+logger.debug { "function called with #{value}" }
+
+```
+
+The logging format has been pre-configured so all logging from PlaceOS is uniform and simple to parse
+
+
+### Metadata
+
+Metadata is used by various components to simplify configuration.
+
+* `generic_name` => the name that should be used in a system to access the module
+* `descriptive_name` => the manufacturers name for the device
+* `description` => notes or any other descriptive information you wish to add
+* `tcp_port` => TCP port the TCP transport should connect to
+* `udp_port` => UDP port the UDP transport should connect to
+* `uri_base` => The HTTP base for any HTTP requests
+* `default_settings` => Defaults or example settings that should be used to configure a module
+
+
+```crystal
+
+class MyDevice < PlaceOS::Driver
+ generic_name :Driver
+ descriptive_name "Driver model Test"
+ description "This is the driver used for testing"
+ tcp_port 22
+ default_settings({
+ name: "Room 123",
+ username: "steve",
+ password: "$encrypt",
+ complex: {
+ crazy_deep: 1223,
+ },
+ })
+
+ # ...
+
+end
+
+```
+
+
+### Security
+
+By default all public functions are exposed for execution.
+However you can limit who is able to execute sensitive functions.
+
+```crystal
+
+@[Security(Level::Administrator)]
+def perform_task(name : String | Int32)
+ queue &.success("hello #{name}")
+end
+
+```
+
+Use the `Security` annotation to define the access level of the function.
+The options are:
+
+* Administrator `Level::Administrator`
+* Support `Level::Support`
+
+
+### Interfaces
+
+Drivers can expose any methods that make sense for the device, service or logic they encapsulate.
+Across these there are often core sets of similar functionality.
+Interfaces provide a standard way of implementing and interacting with this.
+
+Thier usage is optional, but highly encouraged as it both improves modularity and reduces complexity in driver implementations.
+
+A full list of interfaces is [available in the driver framework](https://github.com/PlaceOS/driver/tree/master/src/placeos-driver/interface). This will expand over time to cover common, repeated patterns as they emerge.
+
+#### Implementing an Interface
+
+Each interface is a module containing abstract methods, types and functionality built from these.
+
+First include the module within the driver body.
+```crystal
+include Interface::Powerable
+```
+You will then need to provide implementations of the abstract methods.
+The compiler will guide you in this.
+
+Some interfaces will also provide default implementation for other methods.
+These may be overridden if the device or service provides a more efficient way to directly execute the desired behaviour.
+To keep compatibility, overridden methods must maintain feature and functional parity with the original implementation.
+
+#### Using an Interface
+
+Drivers that provide an Interface can be discovered using the `system.implementing` method from any logic module.
+This will return a list of all drivers in the system which implement the Interface.
+
+Similarly, the `accessor` macro provides a way to declare a dependency on a sibling driver that provides specific functionality.
+
+For more infomaration on these and for usage examples, see [logic drivers](#logic-drivers).
diff --git a/docs/writing-a-spec.md b/docs/writing-a-spec.md
new file mode 100644
index 00000000000..d470973feb7
--- /dev/null
+++ b/docs/writing-a-spec.md
@@ -0,0 +1,231 @@
+# How to write a spec
+
+There are three kind of drivers
+
+* Streaming IO (TCP, SSH, UDP, Multicast, ect)
+* HTTP Client
+* Logic
+
+From a driver code structure standpoint there is no difference between these types.
+
+* The same driver can be used over a TCP, UDP or SSH transport.
+* All drivers support HTTP methods if a URI endpoint is defined.
+* If a driver is associated with a System then it has access to logic helpers
+
+During a test, the loaded module is loaded with a TCP transport, HTTP enabled and logic module capabilities.
+This allows for testing the full capabilities of any driver.
+
+The driver is launched as it would be in production.
+
+
+## Expectations
+
+Specs have access to Crystal lang spec expectations. This allows you to confirm expectations.
+https://crystal-lang.org/api/latest/Spec/Expectations.html
+
+```crystal
+
+variable = 34
+variable.should eq(34)
+
+```
+
+There is a good overview on how to use expectations here: https://crystal-lang.org/reference/guides/testing.html
+
+
+### Status
+
+Expectations are primarily there to test the state of the module.
+
+* You can access state via the status helper: `status[:state_name]`
+* Then you can check it an expected value: `status[:state_name].should eq(14)`
+
+
+## Testing Streaming IO
+
+The following functions are available for testing streaming IO:
+
+* `transmit(data)` -> transmits the object to the module over the streaming IO interface
+* `responds(data)` -> alias for `transmit`
+* `should_send(data, timeout = 500.milliseconds)` -> expects the module to respond with the data provided
+* `expect_send(timeout = 500.milliseconds)` -> returns the next `Bytes` sent by the module (useful if the data sent is not deterministic, i.e. has a time stamp)
+
+A common test case is to ensure that module state updates as expected after transmitting some data to it:
+
+```crystal
+
+# transmit some data
+transmit(">V:2,C:11,G:2001,B:1,S:1,F:100#")
+
+# check that the state updated as expected
+status[:area2001].should eq(1)
+
+```
+
+
+## Testing HTTP requests
+
+The test suite emulates a HTTP server so you can inspect HTTP requests and send canned responses to the module.
+
+```crystal
+
+expect_http_request do |request, response|
+ io = request.body
+ if io
+ data = io.gets_to_end
+ request = JSON.parse(data)
+ if request["message"] == "hello steve"
+ response.status_code = 202
+ else
+ response.status_code = 401
+ end
+ else
+ raise "expected request to include dialing details #{request.inspect}"
+ end
+end
+
+# check that the state updated as expected
+status[:area2001].should eq(1)
+
+```
+
+Use `expect_http_request` to access an expected request coming from the module.
+
+* when the block completes, the response is sent to the module
+* you can see `request` object details here: https://crystal-lang.org/api/latest/HTTP/Request.html
+* you can see `response` object details here: https://crystal-lang.org/api/latest/HTTP/Server/Response.html
+
+
+## Executing functions
+
+This allows you to request actions be performed in the module via the standard public interface.
+
+* `exec(:function_name, argument_name: argument_value)` -> `response` a response future (async return value)
+* You should send and `responds(data)` before inspecting the `response.get`
+
+```crystal
+
+# Execute a command
+response = exec(:scene?, area: 1)
+
+# Check that the command causes the module to send some data
+should_send("?AREA,1,6\r\n")
+# Respond to that command
+responds("~AREA,1,6,2\r\n")
+
+# Check if the functions return value is expected
+response.get.should eq(2)
+# Check if the module state is correct
+status[:area1].should eq(2)
+
+```
+
+
+## Testing Logic
+
+Logic modules typically expect a system to contain some drivers which the logic modules interacts with.
+
+```crystal
+
+# define mock versions of the drivers it will interact with
+
+class Display < DriverSpecs::MockDriver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Inputs
+ HDMI
+ HDMI2
+ VGA
+ VGA2
+ Miracast
+ DVI
+ DisplayPort
+ HDBaseT
+ Composite
+ end
+
+ include PlaceOS::Driver::Interface::InputSelection(Inputs)
+
+ # Configure initial state in on_load
+ def on_load
+ self[:power] = false
+ self[:input] = Inputs::HDMI
+ end
+
+ # implement the abstract methods required by the interfaces
+ def power(state : Bool)
+ self[:power] = state
+ end
+
+ def switch_to(input : Inputs)
+ mute(false)
+ self[:input] = input
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ self[:mute] = state
+ self[:mute0] = state
+ end
+end
+
+```
+
+Then you can define the system configuration,
+you can also change the system configuration throughout your spec to test different configurations.
+
+```crystal
+
+DriverSpecs.mock_driver "Place::LogicExample" do
+
+ # Where `{Display, Display}` is referencing the `MockDriver` class defined above
+ # and `Display:` is the friendly name
+ # so this system would have `Display_1`, `Display_2`, `Switcher_1`
+ system({
+ Display: {Display, Display},
+ Switcher: {Switcher},
+ })
+
+ # ...
+end
+
+```
+
+Along with the physical system configuration you can test different setting configurations.
+Settings can also be changed throughout the life cycle of your spec.
+
+```crystal
+
+DriverSpecs.mock_driver "Place::LogicExample" do
+
+ settings({
+ name: "Meeting Room 1",
+ map_id: "1.03"
+ })
+
+end
+
+```
+
+An action you perform on your driver might be expected to update state in the mock devices.
+You can access this state via the `system` helper
+
+```crystal
+
+DriverSpecs.mock_driver "Place::LogicExample" do
+
+ # execute a function in your logic module
+ exec(:power, true)
+
+ # Check that the expected state has updated in you mock device
+ system(:Display_1)[:power].should eq(true)
+
+end
+
+```
+
+All status queried in this manner is returned as a `JSON::Any` object
diff --git a/drivers/biamp/nexia.cr b/drivers/biamp/nexia.cr
new file mode 100644
index 00000000000..8a0223aada8
--- /dev/null
+++ b/drivers/biamp/nexia.cr
@@ -0,0 +1,210 @@
+module Biamp; end
+
+class Biamp::Nexia < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 23 # Telnet
+ descriptive_name "Biamp Nexia/Audia"
+ generic_name :Mixer
+
+ alias Ids = Array(UInt32) | UInt32
+
+ def on_load
+ # Nexia requires some breathing room
+ queue.wait = false
+ queue.delay = 30.milliseconds
+ transport.tokenizer = Tokenizer.new("\r\n", "\xFF\xFE\x01")
+ end
+
+ def on_update
+ # min -100
+ # max +12
+
+ self["fader_min"] = -36 # specifically for tonsley
+ self["fader_max"] = 12
+ end
+
+ def connected
+ send("\xFF\xFE\x01") # Echo off
+ do_send("GETD", 0, "DEVID")
+
+ schedule.clear
+ schedule.every(60.seconds) do
+ do_send("GETD", 0, "DEVID")
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def preset(number : UInt32)
+ #
+ # Recall Device 0 Preset number 1001
+ # Device Number will always be 0 for Preset strings
+ # 1001 == minimum preset number
+ #
+ do_send("RECALL", 0, "PRESET", number, name: "preset_#{number}")
+ end
+
+ # {1 => [2,3,5], 2 => [2,3,6]}, true
+ # Supports Standard, Matrix and Automixers
+ def mixer(id : UInt32, inouts : Hash(String, Float32 | Array(Float32)) | Array(Float32), mute : Bool = false, type : String = "matrix")
+ value = mute ? 0 : 1
+
+ if inouts.is_a? Hash
+ req = type == "matrix" ? "MMMUTEXP" : "SMMUTEXP"
+
+ inouts.each_key do |input|
+ outputs = inouts[input]
+ outs = ensure_array(outputs)
+
+ outs.each do |output|
+ do_send("SET", self["device_id"]?, req, id, input, output, value)
+ end
+ end
+ else # assume array (auto-mixer)
+ inouts.each do |input|
+ do_send("SET", self["device_id"]?, "AMMUTEXP", id, input, value)
+ end
+ end
+ end
+
+ FADERS = {
+ "fader" => "FDRLVL",
+ "matrix_in" => "MMLVLIN",
+ "matrix_out" => "MMLVLOUT",
+ "matrix_crosspoint" => "MMLVLXP",
+ "stdmatrix_in" => "SMLVLIN",
+ "stdmatrix_out" => "SMLVLOUT",
+ "auto_in" => "AMLVLIN",
+ "auto_out" => "AMLVLOUT",
+ "io_in" => "INPLVL",
+ "io_out" => "OUTLVL",
+ "FDRLVL" => "fader",
+ "MMLVLIN" => "matrix_in",
+ "MMLVLOUT" => "matrix_out",
+ "MMLVLXP" => "matrix_crosspoint",
+ "SMLVLIN" => "stdmatrix_in",
+ "SMLVLOUT" => "stdmatrix_out",
+ "AMLVLIN" => "auto_in",
+ "AMLVLOUT" => "auto_out",
+ "INPLVL" => "io_in",
+ "OUTLVL" => "io_out",
+ }
+
+ def fader(fader_id : Ids, level : Float32, index : Int32 = 1, type : String = "fader")
+ fad_type = FADERS[type]
+
+ # value range: -100 ~ 12
+ faders = ensure_array(fader_id)
+ faders.each do |fad|
+ do_send("SETD", self["device_id"]?, fad_type, fad, index, level, name: "fader_#{fad}")
+ end
+ end
+
+ def faders(ids : Ids, level : Float32, index : Int32 = 1, type : String = "fader", **args)
+ fader(ids, level, index, type)
+ end
+
+ MUTES = {
+ "fader" => "FDRMUTE",
+ "matrix_in" => "MMMUTEIN",
+ "matrix_out" => "MMMUTEOUT",
+ "auto_in" => "AMMUTEIN",
+ "auto_out" => "AMMUTEOUT",
+ "stdmatrix_in" => "SMMUTEIN",
+ "stdmatrix_out" => "SMOUTMUTE",
+ "io_in" => "INPMUTE",
+ "io_out" => "OUTMUTE",
+ "FDRMUTE" => "fader",
+ "MMMUTEIN" => "matrix_in",
+ "MMMUTEOUT" => "matrix_out",
+ "AMMUTEIN" => "auto_in",
+ "AMMUTEOUT" => "auto_out",
+ "SMMUTEIN" => "stdmatrix_in",
+ "SMOUTMUTE" => "stdmatrix_out",
+ "INPMUTE" => "io_in",
+ "OUTMUTE" => "io_out",
+ }
+
+ def mute(fader_id : Ids, val : Bool = true, index : Int32 = 1, type : String = "fader")
+ actual = val ? 1 : 0
+ mute_type = MUTES[type]
+
+ faders = ensure_array(fader_id)
+ faders.each do |fad|
+ do_send("SETD", self["device_id"]?, mute_type, fad, index, actual, name: "mute_#{fad}")
+ end
+ end
+
+ def mutes(ids : Ids, muted : Bool = true, index : Int32 = 1, type : String = "fader", **args)
+ mute(ids, muted, index, type)
+ end
+
+ def unmute(fader_id : Ids, index : Int32 = 1, type : String = "fader")
+ mute(fader_id, false, index, type)
+ end
+
+ def query_fader(fader_id : Ids, index : Int32 = 1, type : String = "fader")
+ fad = ensure_single(fader_id)
+ fad_type = FADERS[type]
+
+ do_send("GETD", self["device_id"]?, fad_type, fad, index)
+ end
+
+ def query_faders(ids : Ids, index : Int32 = 1, type : String = "fader", **args)
+ query_fader(ids, index, type)
+ end
+
+ def query_mute(fader_id : Ids, index : Int32 = 1, type : String = "fader")
+ fad = ensure_single(fader_id)
+ mute_type = MUTES[type]
+
+ do_send("GETD", self["device_id"]?, mute_type, fad, index)
+ end
+
+ def query_mutes(ids : Ids, index : Int32 = 1, type : String = "fader", **args)
+ query_mute(ids, index, type)
+ end
+
+ def received(data, task)
+ data = String.new(data)
+
+ if data =~ /-ERR/
+ return task.try &.abort
+ else
+ logger.debug { "Nexia responded #{data}" }
+ end
+
+ # --> "#SETD 0 FDRLVL 29 1 0.000000 +OK"
+ data = data.split(" ")
+ unless data[2].nil?
+ resp_type = data[2]
+
+ if resp_type == "DEVID"
+ # "#GETD 0 DEVID 1 "
+ self["device_id"] = data[-1].to_i
+ elsif MUTES.has_key?(resp_type)
+ type = MUTES[resp_type]
+ self["#{type}#{data[3]}_#{data[4]}_mute"] = data[5] == "1"
+ elsif FADERS.has_key?(resp_type)
+ type = FADERS[resp_type]
+ self["#{type}#{data[3]}_#{data[4]}"] = data[5]
+ end
+ end
+
+ task.try &.success
+ end
+
+ private def do_send(*args, **options)
+ send("#{args.join(' ')} \n", **options)
+ end
+
+ private def ensure_array(object)
+ object.is_a?(Array) ? object : [object]
+ end
+
+ private def ensure_single(object)
+ object.is_a?(Array) ? object[0] : object
+ end
+end
diff --git a/drivers/biamp/nexia_spec.cr b/drivers/biamp/nexia_spec.cr
new file mode 100644
index 00000000000..3680469d5ec
--- /dev/null
+++ b/drivers/biamp/nexia_spec.cr
@@ -0,0 +1,52 @@
+DriverSpecs.mock_driver "Biamp::Nexia" do
+ should_send "\xFF\xFE\x01"
+ should_send("GETD 0 DEVID")
+
+ exec(:preset, 1001)
+ should_send("RECALL 0 PRESET 1001")
+
+ exec(:fader, 1, -100)
+ should_send("SETD FDRLVL 1 1 -100.0")
+ responds("SETD FDRLVL 1 1 -100.0 \r\n")
+ status["fader1_1"].should eq("-100.0")
+
+ exec(:faders, 1, -75, 2, "matrix_in")
+ should_send("SETD MMLVLIN 1 2 -75.0")
+ responds("SETD MMLVLIN 1 2 -75.0 \r\n")
+ status["matrix_in1_2"].should eq("-75.0")
+
+ exec(:mute, 1234, false, 3)
+ should_send("SETD FDRMUTE 1234 3 0")
+ responds("SETD FDRMUTE 1234 3 0 \r\n")
+ status["fader1234_3_mute"].should eq(false)
+
+ exec(:mutes, 1234, true, 5, "auto_in")
+ should_send("SETD AMMUTEIN 1234 5 1")
+ responds("SETD AMMUTEIN 1234 5 1 \r\n")
+ status["auto_in1234_5_mute"].should eq(true)
+
+ exec(:unmute, 111)
+ should_send("SETD FDRMUTE 111 1 0")
+ responds("SETD FDRMUTE 111 1 0 \r\n")
+ status["fader111_1_mute"].should eq(false)
+
+ exec(:query_fader, 133)
+ should_send("GETD FDRLVL 133 1 ")
+ responds("GETD FDRLVL 133 1 -100.0 \r\n")
+ status["fader133_1"].should eq("-100.0")
+
+ exec(:query_faders, 144)
+ should_send("GETD FDRLVL 144 1 ")
+ responds("GETD FDRLVL 144 1 -80.0 \r\n")
+ status["fader144_1"].should eq("-80.0")
+
+ exec(:query_mute, 155)
+ should_send("GETD FDRMUTE 155 1 ")
+ responds("GETD FDRMUTE 155 1 0 \r\n")
+ status["fader155_1_mute"].should eq(false)
+
+ exec(:query_mutes, 166)
+ should_send("GETD FDRMUTE 166 1 ")
+ responds("GETD FDRMUTE 166 1 1 \r\n")
+ status["fader166_1_mute"].should eq(true)
+end
diff --git a/drivers/biamp/tesira.cr b/drivers/biamp/tesira.cr
new file mode 100644
index 00000000000..33ae6da97cd
--- /dev/null
+++ b/drivers/biamp/tesira.cr
@@ -0,0 +1,221 @@
+require "telnet"
+
+module Biamp; end
+
+class Biamp::Tesira < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 23 # Telnet
+ descriptive_name "Biamp Tesira"
+ generic_name :Mixer
+
+ default_settings({
+ no_password: true,
+ username: "default",
+ password: "default",
+ })
+
+ alias Num = Int32 | Float64
+ alias Ids = String | Array(String)
+
+ def on_load
+ # Nexia requires some breathing room
+ queue.wait = false
+ queue.delay = 30.milliseconds
+ end
+
+ def connected
+ @telnet = telnet = Telnet.new do |telnet_response|
+ transport.send telnet_response
+ end
+ transport.pre_processor { |bytes| telnet.buffer(bytes) }
+
+ if setting(Bool, :no_password)
+ do_send setting(String, :username) || "admin", wait: false, delay: 200.milliseconds, priority: 98
+ do_send setting(String, :password), wait: false, delay: 200.milliseconds, priority: 97
+ end
+ do_send "SESSION set verbose false", priority: 96
+
+ schedule.clear
+ schedule.every(60.seconds) do
+ do_send "DEVICE get serialNumber", priority: 95
+ end
+ end
+
+ def disconnected
+ transport.tokenizer = nil
+ schedule.clear
+ end
+
+ def preset(number_or_name : String | Int32)
+ if number_or_name.is_a? Int32
+ do_send "DEVICE recallPreset #{number_or_name}", priority: 30, name: "preset_#{number_or_name}"
+ else
+ do_send build(:DEVICE, :recallPresetByName, number_or_name), priority: 30, name: "preset_#{number_or_name}"
+ end
+ end
+
+ def start_audio
+ do_send "DEVICE startAudio"
+ end
+
+ def reboot
+ do_send "DEVICE reboot"
+ end
+
+ def get_aliases
+ do_send "SESSION get aliases"
+ end
+
+ MIXERS = {
+ "matrix" => "crosspointLevelState",
+ "mixer" => "crosspoint",
+ }
+
+ def mixer(id : String, inouts : Hash(Int32, Int32 | Array(Int32)) | Array(Int32), mute : Bool = false, type : String = "matrix")
+ mixer_type = MIXERS[type] || type
+
+ if inouts.is_a? Hash
+ inouts.each do |input, outs|
+ outputs = ensure_array(outs)
+ outputs.each do |output|
+ do_send build(id, :set, mixer_type, input, output, mute), priority: 30, name: "mixmute_#{input}_#{output}"
+ end
+ end
+ else # assume array (auto-mixer)
+ inouts.each do |input|
+ do_send build(id, :set, mixer_type, input, mute), priority: 30, name: "mixmute_#{input}"
+ end
+ end
+ end
+
+ FADERS = {
+ "fader" => "level",
+ "matrix_in" => "inputLevel",
+ "matrix_out" => "outputLevel",
+ "matrix_crosspoint" => "crosspointLevel",
+ "level" => "fader",
+ "inputLevel" => "matrix_in",
+ "outputLevel" => "matrix_out",
+ "crosspointLevel" => "matrix_crosspoint",
+ }
+
+ def fader(fader_id : Ids, level : Num | Bool, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ # value range: -100 ~ 12
+ fader_type = FADERS[type] || type
+
+ fader_ids = ensure_array(fader_id)
+ indicies = ensure_array(index)
+ fader_ids.each do |fad|
+ indicies.each do |i|
+ do_send build(fad, :set, fader_type, i, level), priority: 30, name: "fade_#{fad}_#{i}"
+ self["#{fader_type}_#{fad}_#{i}"] = level
+ end
+ end
+ end
+
+ # Named params version
+ def faders(ids : Ids, level : Num | Bool, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ fader(ids, level, index, type)
+ end
+
+ MUTES = {
+ "fader" => "mute",
+ "matrix_in" => "inputMute",
+ "matrix_out" => "outputMute",
+ "mute" => "fader",
+ "inputMute" => "matrix_in",
+ "outputMute" => "matrix_out",
+ }
+
+ def mute(fader_id : Ids, value : Bool = true, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ mute_type = MUTES[type] || type
+
+ fader_ids = ensure_array(fader_id)
+ indicies = ensure_array(index)
+ fader_ids.each do |fad|
+ indicies.each do |i|
+ do_send build(fad, :set, mute_type, i, value), priority: 30, name: "mute_#{fad}_#{i}"
+ self["#{mute_type}_#{fad}_#{i}_mute"] = value
+ end
+ end
+ end
+
+ # Named params version
+ def mutes(ids : Ids, muted : Bool, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ mute(ids, muted, index, type)
+ end
+
+ def unmute(fader_id : Ids, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ mute(fader_id, false, index, type)
+ end
+
+ def query_fader(fader_id : Ids, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ fad_type = FADERS[type] || type
+ fader_id = ensure_array(fader_id)[0]
+ index = ensure_array(index)[0]
+
+ do_send build(fader_id, :get, fad_type, index)
+ end
+
+ # Named params version
+ def query_faders(ids : Ids, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ query_fader(ids, index, type)
+ end
+
+ def query_mute(fader_id : Ids, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ mute_type = MUTES[type] || type
+ fader_id = ensure_array(fader_id)[0]
+ index = ensure_array(index)[0]
+
+ do_send build(fader_id, :get, mute_type, index)
+ end
+
+ # Named params version
+ def query_mutes(ids : Ids, index : Int32 | Array(Int32) = 1, type : String = "fader")
+ query_mute(ids, index, type)
+ end
+
+ def received(data, task)
+ data = String.new(data).strip
+
+ logger.debug { "Tesira responded -> data: #{data}" }
+ result = data.split(" ")
+
+ if result[0] == "-"
+ task.try(&.abort)
+ end
+
+ if data =~ /login:|server/i
+ transport.tokenizer = Tokenizer.new "\r\n"
+ end
+
+ task.try(&.success)
+ end
+
+ private def build(*args)
+ cmd = ""
+ args.each do |arg|
+ data = arg.to_s
+ next if data.blank?
+ cmd = cmd + " " if cmd.size > 0
+
+ if data.includes? " "
+ cmd = cmd + "\""
+ cmd = cmd + data
+ cmd = cmd + "\""
+ else
+ cmd = cmd + data
+ end
+ end
+ cmd
+ end
+
+ private def do_send(command, **options)
+ logger.debug { "requesting #{command}" }
+ send @telnet.not_nil!.prepare(command), **options
+ end
+
+ private def ensure_array(object)
+ object.is_a?(Array) ? object : [object]
+ end
+end
diff --git a/drivers/biamp/tesira_spec.cr b/drivers/biamp/tesira_spec.cr
new file mode 100644
index 00000000000..c12c7362d02
--- /dev/null
+++ b/drivers/biamp/tesira_spec.cr
@@ -0,0 +1,37 @@
+DriverSpecs.mock_driver "Biamp::Tesira" do
+ transmit "login: "
+ should_send "default\r\n"
+ should_send "default\r\n"
+ should_send "SESSION set verbose false\r\n"
+
+ exec(:preset, 1001)
+ should_send "DEVICE recallPreset 1001"
+
+ exec(:preset, "1001-test")
+ should_send "DEVICE recallPresetByName 1001-test"
+
+ exec(:start_audio)
+ should_send "DEVICE startAudio"
+
+ exec(:reboot)
+ should_send "DEVICE reboot"
+
+ exec(:get_aliases)
+ should_send "SESSION get aliases"
+
+ exec(:mixer, "123", [1])
+ should_send "123 set crosspointLevelState 1 false"
+
+ exec(:fader, "Fader123", 11)
+ should_send "Fader123 set level 1 11"
+ responds("+OK\r\n")
+ status["level_Fader123_1"] = 11
+
+ exec(:mute, "Fader123")
+ should_send "Fader123 set mute 1 true"
+ responds("+OK\r\n")
+ status["level_Fader123_1_mute"] = true
+
+ exec(:query_fader, "Fader123")
+ should_send "Fader123 get level 1"
+end
diff --git a/drivers/bose/control_space_serial.cr b/drivers/bose/control_space_serial.cr
new file mode 100644
index 00000000000..a2721f7b0b4
--- /dev/null
+++ b/drivers/bose/control_space_serial.cr
@@ -0,0 +1,58 @@
+module Bose; end
+
+# Documentation: https://aca.im/driver_docs/Bose/Bose-ControlSpace-SerialProtocol-v5.pdf
+
+class Bose::ControlSpaceSerial < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 10055
+ descriptive_name "Bose ControlSpace Serial Protocol"
+ generic_name :Mixer
+
+ def on_load
+ # 0x0D ( carriage return \r)
+ transport.tokenizer = Tokenizer.new(Bytes[0x0D])
+ on_update
+ end
+
+ def on_update
+ end
+
+ def connected
+ schedule.every(60.seconds) do
+ logger.debug { "-- maintaining connection" }
+ do_send "GS", priority: 99
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ private def do_send(data, **options)
+ logger.debug { "requesting: #{data}" }
+ send "#{data}\x0D", **options
+ end
+
+ def set_parameter_group(id : UInt8)
+ do_send("SS #{id.to_s(16).upcase}", wait: false, name: "set_pgroup").get
+ self[:parameter_group] = id
+ end
+
+ def get_parameter_group
+ do_send "GS"
+ end
+
+ def received(data, task)
+ # Ignore the framing bytes
+ data = String.new(data).rchop
+ logger.debug { "ControlSpace sent: #{data}" }
+
+ parts = data.split(" ")
+ case parts[0]
+ when "S"
+ self[:parameter_group] = parts[1].to_i(16)
+ end
+
+ task.try &.success
+ end
+end
diff --git a/drivers/bose/control_space_serial_spec.cr b/drivers/bose/control_space_serial_spec.cr
new file mode 100644
index 00000000000..f5685cafbd0
--- /dev/null
+++ b/drivers/bose/control_space_serial_spec.cr
@@ -0,0 +1,10 @@
+DriverSpecs.mock_driver "Bose::ControlSpaceSerial" do
+ exec(:set_parameter_group, 12)
+ should_send("SS C\r")
+ status[:parameter_group].should eq(12)
+
+ exec(:get_parameter_group)
+ should_send("GS\r")
+ responds("S FF\r")
+ status[:parameter_group].should eq(255)
+end
diff --git a/drivers/cisco/dna_spaces.cr b/drivers/cisco/dna_spaces.cr
new file mode 100644
index 00000000000..d5147306cd5
--- /dev/null
+++ b/drivers/cisco/dna_spaces.cr
@@ -0,0 +1,631 @@
+module Cisco; end
+
+require "set"
+require "jwt"
+require "s2_cells"
+require "simple_retry"
+require "placeos-driver/interface/locatable"
+
+class Cisco::DNASpaces < PlaceOS::Driver
+ include Interface::Locatable
+
+ # Discovery Information
+ descriptive_name "Cisco DNA Spaces"
+ generic_name :DNA_Spaces
+ uri_base "https://partners.dnaspaces.io"
+
+ default_settings({
+ dna_spaces_activation_key: "provide this and the API / tenant ids will be generated automatically",
+ dna_spaces_api_key: "X-API-KEY",
+ tenant_id: "sfdsfsdgg",
+
+ # Time before a user location is considered probably too old (in minutes)
+ max_location_age: 10,
+
+ floorplan_mappings: {
+ location_a4cb0: {
+ "level_name" => "optional name",
+ "building" => "zone-GAsXV0nc",
+ "level" => "zone-GAsmleH",
+ "offset_x" => 12.4,
+ "offset_y" => 5.2,
+ "map_width" => 50.3,
+ "map_height" => 100.9,
+ },
+ },
+
+ debug_stream: false,
+ })
+
+ @streaming = false
+ @last_received = 0_i64
+ @stream_active = false
+
+ def on_load
+ on_update
+ if !@api_key.empty?
+ @streaming = true
+ spawn(same_thread: true) { start_streaming_events }
+ end
+ end
+
+ def on_unload
+ @terminated = true
+ @channel.close
+ @stream_active = false
+ update_monitoring_status(running: false)
+ end
+
+ @activation_token : String = ""
+ @api_key : String = ""
+ @tenant_id : String = ""
+ @terminated : Bool = false
+ @channel : Channel(String) = Channel(String).new
+ @max_location_age : Time::Span = 10.minutes
+ @s2_level : Int32 = 21
+ @floorplan_mappings : Hash(String, Hash(String, String | Float64)) = Hash(String, Hash(String, String | Float64)).new
+ @debug_stream : Bool = false
+ @events_received : UInt64 = 0_u64
+
+ def on_update
+ @max_location_age = (setting?(UInt32, :max_location_age) || 10).minutes
+ @s2_level = setting?(Int32, :s2_level) || 21
+ @floorplan_mappings = setting?(Hash(String, Hash(String, String | Float64)), :floorplan_mappings) || @floorplan_mappings
+ @debug_stream = setting?(Bool, :debug_stream) || false
+
+ schedule.clear
+ schedule.every(30.minutes) { cleanup_caches }
+ schedule.every(5.minutes) { update_monitoring_status }
+ schedule.in(5.seconds) { update_monitoring_status }
+
+ @activation_token = setting?(String, :dna_spaces_activation_key) || ""
+ if @activation_token.empty?
+ @api_key = setting(String, :dna_spaces_api_key)
+ @tenant_id = setting(String, :tenant_id)
+ else
+ @api_key = setting?(String, :dna_spaces_api_key) || ""
+ @tenant_id = setting?(String, :tenant_id) || ""
+
+ # Activate the API key using the activation_token
+ schedule.in(5.seconds) { activate } if @api_key.empty?
+ end
+
+ if !@streaming && !@api_key.empty?
+ @streaming = true
+ spawn(same_thread: true) { start_streaming_events }
+ end
+ end
+
+ @[Security(Level::Support)]
+ def activate
+ return if @activation_token.empty?
+
+ response = get("/client/v1/partner/partnerPublicKey/")
+ raise "failed to obtain partner public key, code #{response.status_code}" unless response.success?
+
+ logger.debug { "public key requested: #{response.body}" }
+
+ payload = NamedTuple(
+ status: Bool,
+ message: String,
+ data: Array(ActivactionPublicKey)).from_json(response.body.not_nil!)
+
+ raise "unexpected failure obtaining partner public key: #{payload[:message]}" unless payload[:status]
+
+ public_key = payload[:data][0].public_key
+ payload, header = JWT.decode(@activation_token, public_key, JWT::Algorithm::RS256)
+ app_id = payload["appId"].as_s
+ ref_id = payload["activationRefId"].as_s
+ tenant_id = payload["tenantId"].as_i64.to_s
+
+ response = post("/client/v1/partner/activateOnPremiseApp", headers: {
+ "Content-Type" => "application/json",
+ "Authorization" => "Bearer #{@activation_token}",
+ }, body: {
+ appId: app_id,
+ activationRefId: ref_id,
+ }.to_json)
+ raise "failed to obtain API key, code #{response.status_code}\n#{response.body}" unless response.success?
+
+ logger.debug { "application activated: #{response.body}" }
+
+ payload = NamedTuple(
+ status: Bool,
+ message: String,
+ data: NamedTuple(apiKey: String)).from_json(response.body.not_nil!)
+
+ raise "unexpected failure obtaining API key: #{payload[:message]}" unless payload[:status]
+
+ api_key = payload[:data][:apiKey]
+ logger.debug { "saving API key: #{tenant_id}, #{api_key}" }
+
+ define_setting(:tenant_id, tenant_id)
+ define_setting(:dna_spaces_api_key, api_key)
+ define_setting(:dna_spaces_activation_key, "")
+
+ logger.debug { "settings saved! Starting stream" }
+ @api_key = api_key
+ @tenant_id = tenant_id
+ if !@streaming
+ @streaming = true
+ spawn(same_thread: true) { start_streaming_events }
+ end
+ end
+
+ class LocationInfo
+ include JSON::Serializable
+
+ getter location : Location
+
+ @[JSON::Field(key: "locationDetails")]
+ getter details : LocationDetails
+ end
+
+ def get_location_info(location_id : String)
+ response = get("/api/partners/v1/locations/#{location_id}?partnerTenantId=#{@tenant_id}", headers: {
+ "X-API-KEY" => @api_key,
+ })
+
+ raise "failed to obtain location id #{location_id}, code #{response.status_code}" unless response.success?
+ LocationInfo.from_json(response.body.not_nil!)
+ end
+
+ @description_lock : Mutex = Mutex.new
+ @location_descriptions : Hash(String, String) = {} of String => String
+
+ def seen_locations
+ @description_lock.synchronize { @location_descriptions.dup }
+ end
+
+ # MAC Address => Location (including user)
+ @locations : Hash(String, DeviceLocationUpdate) = {} of String => DeviceLocationUpdate
+ @loc_lock : Mutex = Mutex.new
+
+ def locations
+ @loc_lock.synchronize { yield @locations }
+ end
+
+ @user_lookup : Hash(String, Set(String)) = {} of String => Set(String)
+ @user_loc : Mutex = Mutex.new
+
+ def user_lookup
+ @user_loc.synchronize { yield @user_lookup }
+ end
+
+ def user_lookup(user_id : String)
+ formatted_user = format_username(user_id)
+ user_lookup { |lookup| lookup[formatted_user]? }
+ end
+
+ def locate_mac(address : String)
+ formatted_address = format_mac(address)
+ locations { |locs| locs[formatted_address]? }
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def inspect_state
+ logger.debug {
+ "MAC Locations: #{locations &.keys}"
+ }
+ {tracking: locations &.size, events_received: @events_received}
+ end
+
+ @map_details : Hash(String, Dimension) = {} of String => Dimension
+ @map_lock : Mutex = Mutex.new
+
+ def get_map_details(map_id : String)
+ map = @map_lock.synchronize { @map_details[map_id]? }
+ if !map
+ response = get("/api/partners/v1/maps/#{map_id}?partnerTenantId=#{@tenant_id}", headers: {
+ "X-API-KEY" => @api_key,
+ })
+ if !response.success?
+ message = "failed to obtain map id #{map_id}, code #{response.status_code}"
+ logger.warn { message }
+ return nil
+ end
+ map = MapInfo.from_json(response.body.not_nil!).dimension
+ @map_lock.synchronize { @map_details[map_id] = map }
+ end
+ map
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def cleanup_caches : Nil
+ logger.debug { "removing location data that is over 30 minutes old" }
+
+ old = 30.minutes.ago.to_unix
+ remove_keys = [] of String
+ locations do |locs|
+ locs.each { |mac, location| remove_keys << mac if location.last_seen < old }
+ remove_keys.each { |mac| locs.delete(mac) }
+ end
+
+ logger.debug { "removed #{remove_keys.size} MACs" }
+ nil
+ end
+
+ # we want to stream events until driver is terminated
+ protected def start_streaming_events
+ @streaming = true
+ SimpleRetry.try_to(
+ base_interval: 10.milliseconds,
+ max_interval: 5.seconds
+ ) { stream_events unless @terminated }
+ ensure
+ @streaming = false
+ end
+
+ # as sometimes the map id is missing, but in the same location
+ # location id => map id
+ @location_id_maps = {} of String => String
+
+ # Processes events as they come in, forces a disconnect if no events are sent
+ # for a period of time as the remote should be sending them periodically
+ protected def process_events(client)
+ loop do
+ select
+ when data = @channel.receive
+ logger.debug { "received push #{data}" } if @debug_stream
+ @events_received = @events_received &+ 1_u64
+ begin
+ event = Cisco::DNASpaces::Events.from_json(data)
+ payload = event.payload
+ case payload
+ when DeviceExit
+ device_mac = format_mac(payload.device.mac_address)
+ locations &.delete(device_mac)
+ when DeviceEntry
+ # This is used entirely for
+ @description_lock.synchronize { payload.location.descriptions(@location_descriptions) }
+ when DeviceLocationUpdate
+ # Keep track of device location
+ device_mac = format_mac(payload.device.mac_address)
+ existing = nil
+
+ # ignore locations where we don't have enough details to put the device on a map
+ if payload.map_id.presence
+ @location_id_maps[payload.location.location_id] = payload.map_id
+ else
+ found = false
+ payload.location_mappings.values.each do |loc_id|
+ if map_id = @location_id_maps[loc_id]?
+ payload.map_id = map_id
+ found = true
+ break
+ end
+ end
+
+ if !found
+ logger.debug { "ignoring device #{device_mac} location as map_id is empty, location id #{payload.location.location_id}, visit #{payload.visit_id}" }
+ next
+ end
+ end
+
+ payload.last_seen = payload.last_seen // 1000
+
+ locations do |loc|
+ existing = loc[device_mac]?
+ loc[device_mac] = payload
+ end
+
+ # Maintain user lookup
+ if payload.raw_user_id.presence
+ user_id = format_username(payload.raw_user_id)
+
+ if existing && payload.raw_user_id != existing.raw_user_id
+ old_user_id = format_username(existing.raw_user_id)
+
+ user_lookup do |lookup|
+ lookup[old_user_id]?.try &.delete(device_mac)
+ devices = lookup[old_user_id]? || Set(String).new
+ devices.delete(device_mac)
+ lookup.delete(old_user_id) if devices.empty?
+
+ devices = lookup[user_id]? || Set(String).new
+ devices << device_mac
+ lookup[user_id] = devices
+ end
+ else
+ user_lookup do |lookup|
+ devices = lookup[user_id]? || Set(String).new
+ devices << device_mac
+ lookup[user_id] = devices
+ end
+ end
+ end
+
+ # payload.location_mappings => { "ZONE" => loc_id, "FLOOR" => loc_id, "BUILDING" => loc_id, "CAMPUS" => loc_id }
+ else
+ logger.debug { "ignoring event: #{payload ? payload.class : event.class}" }
+ end
+ rescue error
+ logger.error(exception: error) { "parsing DNA Spaces event: #{data}" }
+ end
+ when timeout(20.seconds)
+ logger.debug { "no events received for 20 seconds, expected heartbeat at 15 seconds" }
+ @channel.close
+ break
+ end
+ end
+ ensure
+ client.close
+ end
+
+ protected def stream_events
+ client = HTTP::Client.new URI.parse(config.uri.not_nil!)
+ client.get("/api/partners/v1/firehose/events", HTTP::Headers{
+ "X-API-KEY" => @api_key,
+ }) do |response|
+ if !response.success?
+ @stream_active = false
+ logger.warn { "failed to connect to firehose api #{response.status_code}" }
+ raise "failed to connect to firehose api #{response.status_code}"
+ end
+
+ @stream_active = true
+
+ # We use a channel for event processing so we can make use of timeouts
+ @channel = Channel(String).new
+ spawn(same_thread: true) { process_events(client) }
+
+ begin
+ loop do
+ if response.body_io.closed?
+ @channel.close
+ break
+ end
+
+ if data = response.body_io.gets
+ @last_received = Time.utc.to_unix_ms
+ @channel.send data
+ else
+ @channel.close
+ break
+ end
+ end
+ rescue IO::Error
+ @channel.close
+ end
+ end
+
+ # Trigger the retry behaviour
+ @stream_active = false
+ raise "stream closed"
+ end
+
+ # =============================
+ # Locatable interface
+ # =============================
+ def locate_user(email : String? = nil, username : String? = nil)
+ if macs = user_lookup(username.presence || email.presence.not_nil!)
+ location_max_age = @max_location_age.ago.to_unix
+
+ macs.compact_map { |mac|
+ if location = locate_mac(mac)
+ if location.last_seen > location_max_age
+ # we update the mac_address to a formatted version
+ location.device.mac_address = mac
+ location
+ end
+ end
+ }.sort! { |a, b|
+ b.last_seen <=> a.last_seen
+ }.map { |location|
+ lat = location.latitude
+ lon = location.longitude
+
+ loc = {
+ "location" => "wireless",
+ "coordinates_from" => "top-left",
+ "x" => location.x_pos,
+ "y" => location.y_pos,
+ "lon" => lon,
+ "lat" => lat,
+ "s2_cell_id" => S2Cells::LatLon.new(lat, lon).to_token(@s2_level),
+ "mac" => location.device.mac_address,
+ "variance" => location.unc,
+ "last_seen" => location.last_seen,
+ "dna_floor_id" => location.map_id,
+ "ssid" => location.ssid,
+ "manufacturer" => location.device.manufacturer,
+ "os" => location.device.os,
+ }
+
+ map_width = 0.0
+ map_height = 0.0
+ offset_x = 0.0
+ offset_y = 0.0
+
+ # Add our zone IDs to the response
+ location.location_mappings.each_value do |location_id|
+ if level_data = @floorplan_mappings[location_id]?
+ level_data.each do |key, value|
+ case key
+ when "offset_x"
+ offset_x = value.as(Float64)
+ loc["x"] = location.x_pos - offset_x
+ when "offset_y"
+ offset_y = value.as(Float64)
+ loc["y"] = location.y_pos - offset_y
+ when "map_width"
+ map_width = value.as(Float64)
+ when "map_height"
+ map_height = value.as(Float64)
+ else
+ loc[key] = value
+ end
+ end
+ break
+ end
+ end
+
+ # Add map information to the response
+ if map_width > 0.0 && map_height > 0.0
+ loc["map_width"] = map_width
+ loc["map_height"] = map_height
+ elsif map_size = get_map_details(location.map_id)
+ loc["map_width"] = map_width > 0.0 ? map_width : (map_size.length - offset_x)
+ loc["map_height"] = map_height > 0.0 ? map_height : (map_size.width - offset_y)
+ end
+
+ loc
+ }
+ else
+ [] of Nil
+ end
+ end
+
+ # Will return an array of MAC address strings
+ # lowercase with no seperation characters abcdeffd1234 etc
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ user_lookup(username.presence || email.presence.not_nil!).try(&.to_a) || [] of String
+ end
+
+ # Will return `nil` or `{"location": "wireless", "assigned_to": "bob123", "mac_address": "abcd"}`
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ if location = locate_mac(mac_address)
+ {
+ location: "wireless",
+ assigned_to: format_username(location.raw_user_id),
+ mac_address: format_mac(mac_address),
+ }
+ end
+ end
+
+ # Will return an array of devices and their x, y coordinates
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "looking up device locations in #{zone_id}" }
+ return [] of Nil if location.presence && location != "wireless"
+
+ # Find the floors associated with the provided zone id
+ floors = [] of String
+ adjustments = {} of String => Tuple(Float64, Float64, Float64, Float64)
+ @floorplan_mappings.each do |floor_id, data|
+ if data.values.includes?(zone_id)
+ floors << floor_id
+ offset_x = (data["offset_x"]? || 0.0).as(Float64)
+ offset_y = (data["offset_y"]? || 0.0).as(Float64)
+ map_width = (data["map_width"]? || -1.0).as(Float64)
+ map_height = (data["map_height"]? || -1.0).as(Float64)
+ adjustments[floor_id] = {offset_x, offset_y, map_width, map_height}
+ end
+ end
+ logger.debug { "found matching meraki floors: #{floors}" }
+ return [] of Nil if floors.empty?
+
+ checking_count = @locations.size
+ wrong_floor = 0
+ too_old = 0
+
+ # Find the devices that are on the matching floors
+ oldest_location = @max_location_age.ago.to_unix
+
+ matching = locations(&.compact_map { |mac, loc|
+ if loc.last_seen < oldest_location
+ too_old += 1
+ next
+ end
+ if (floors & loc.location_mappings.values).empty?
+ wrong_floor += 1
+ next
+ end
+
+ # ensure the formatted mac is being used
+ loc.device.mac_address = mac
+ loc
+ })
+
+ logger.debug { "found #{matching.size} matching devices\nchecked #{checking_count} locations, #{wrong_floor} were on the wrong floor, #{too_old} were too old" }
+
+ matching.group_by(&.map_id).flat_map { |map_id, locations|
+ map_width = -1.0
+ map_height = -1.0
+ offset_x = 0.0
+ offset_y = 0.0
+
+ # any adjustments required for these locations?
+ locations.first.location_mappings.each_value do |location_id|
+ if level_data = adjustments[location_id]?
+ offset_x, offset_y, map_width, map_height = level_data
+ break
+ end
+ end
+
+ if map_width == -1.0 || map_height == -1.0
+ if map_size = get_map_details(map_id)
+ map_width = map_width > -1.0 ? map_width : (map_size.length - offset_x)
+ map_height = map_height > -1.0 ? map_height : (map_size.width - offset_y)
+ end
+ end
+
+ locations.map do |loc|
+ lat = loc.latitude
+ lon = loc.longitude
+
+ {
+ location: :wireless,
+ coordinates_from: "top-left",
+ x: loc.x_pos - offset_x,
+ y: loc.y_pos - offset_y,
+ lon: lon,
+ lat: lat,
+ s2_cell_id: S2Cells::LatLon.new(lat, lon).to_token(@s2_level),
+ mac: loc.device.mac_address,
+ variance: loc.unc,
+ last_seen: loc.last_seen,
+ map_width: map_width,
+ map_height: map_height,
+ ssid: loc.ssid,
+ manufacturer: loc.device.manufacturer,
+ os: loc.device.os,
+ }
+ end
+ }
+ end
+
+ def format_mac(address : String)
+ address.gsub(/(0x|[^0-9A-Fa-f])*/, "").downcase
+ end
+
+ def format_username(user : String)
+ if user.includes? "@"
+ user = user.split("@")[0]
+ elsif user.includes? "\\"
+ user = user.split("\\")[1]
+ end
+ user.downcase
+ end
+
+ # This provides the DNA Spaces dashboard with stream consumption status
+ @[Security(PlaceOS::Driver::Level::Administrator)]
+ def update_monitoring_status(running : Bool = true) : Nil
+ response = put("/api/partners/v1/monitoring/status", headers: {
+ "Content-Type" => "application/json",
+ "X-API-KEY" => @api_key,
+ }, body: {
+ data: {
+ overallStatus: {
+ status: running ? "up" : "down",
+ notices: [] of Nil,
+ },
+ instanceDetails: {
+ ipAddress: "",
+ instanceId: module_id,
+ },
+ cloudFirehose: {
+ status: @stream_active ? "connected" : "disconnected",
+ lastReceived: @last_received,
+ },
+ localFirehose: {
+ status: "disconnected",
+ lastReceived: 0,
+ },
+ subsystems: [] of Nil,
+ },
+ }.to_json)
+ raise "failed to update status, code #{response.status_code}\n#{response.body}" unless response.success?
+ end
+end
+
+require "./dna_spaces/events"
diff --git a/drivers/cisco/dna_spaces/activation_publickey.cr b/drivers/cisco/dna_spaces/activation_publickey.cr
new file mode 100644
index 00000000000..eb30ec06dd3
--- /dev/null
+++ b/drivers/cisco/dna_spaces/activation_publickey.cr
@@ -0,0 +1,14 @@
+require "./events"
+
+class Cisco::DNASpaces::ActivactionPublicKey
+ include JSON::Serializable
+
+ getter version : String
+
+ @[JSON::Field(key: "publicKey")]
+ getter public_key : String
+
+ def public_key
+ "-----BEGIN PUBLIC KEY-----\n#{@public_key}\n-----END PUBLIC KEY-----\n"
+ end
+end
diff --git a/drivers/cisco/dna_spaces/app_activaction.cr b/drivers/cisco/dna_spaces/app_activaction.cr
new file mode 100644
index 00000000000..36202b21424
--- /dev/null
+++ b/drivers/cisco/dna_spaces/app_activaction.cr
@@ -0,0 +1,21 @@
+require "./events"
+
+class Cisco::DNASpaces::AppActivaction
+ include JSON::Serializable
+
+ @[JSON::Field(key: "spacesTenantName")]
+ getter spaces_tenant_name : String
+
+ @[JSON::Field(key: "spacesTenantId")]
+ getter spaces_tenant_id : String
+
+ @[JSON::Field(key: "partnerTenantId")]
+ getter partner_tenant_id : String
+ getter name : String
+
+ @[JSON::Field(key: "referenceId")]
+ getter reference_id : String
+
+ @[JSON::Field(key: "instanceName")]
+ getter instance_name : String
+end
diff --git a/drivers/cisco/dna_spaces/device.cr b/drivers/cisco/dna_spaces/device.cr
new file mode 100644
index 00000000000..7f6c09daaed
--- /dev/null
+++ b/drivers/cisco/dna_spaces/device.cr
@@ -0,0 +1,39 @@
+require "./events"
+
+class Cisco::DNASpaces::Device
+ include JSON::Serializable
+
+ @[JSON::Field(key: "deviceId")]
+ getter device_id : String
+
+ @[JSON::Field(key: "userId")]
+ getter user_id : String
+
+ getter tags : Array(String)
+ getter mobile : String
+ getter email : String
+ getter gender : String
+
+ @[JSON::Field(key: "firstName")]
+ getter first_name : String
+
+ @[JSON::Field(key: "lastName")]
+ getter last_name : String
+
+ @[JSON::Field(key: "postalCode")]
+ getter postal_code : String
+
+ # optIns
+ # otherFields
+ # socialNetworkInfo
+
+ # We make this editable so we can store the formatted version here
+ @[JSON::Field(key: "macAddress")]
+ property mac_address : String
+ getter manufacturer : String
+ getter os : String
+
+ @[JSON::Field(key: "osVersion")]
+ getter os_version : String
+ getter type : String
+end
diff --git a/drivers/cisco/dna_spaces/device_count.cr b/drivers/cisco/dna_spaces/device_count.cr
new file mode 100644
index 00000000000..c799d9b8286
--- /dev/null
+++ b/drivers/cisco/dna_spaces/device_count.cr
@@ -0,0 +1,22 @@
+require "./events"
+
+class Cisco::DNASpaces::DeviceCount
+ include JSON::Serializable
+
+ getter location : Location
+
+ @[JSON::Field(key: "associatedCount")]
+ getter associated_count : Int32
+
+ @[JSON::Field(key: "estimatedProbingCount")]
+ getter estimated_probing_count : Int32
+
+ @[JSON::Field(key: "probingRandomizedPercentage")]
+ getter probing_randomized_percentage : Float64
+
+ @[JSON::Field(key: "estimatedDensity")]
+ getter estimated_density : Float64
+
+ @[JSON::Field(key: "estimatedCapacityPercentage")]
+ getter estimated_capacity_percentage : Float64
+end
diff --git a/drivers/cisco/dna_spaces/device_entry.cr b/drivers/cisco/dna_spaces/device_entry.cr
new file mode 100644
index 00000000000..befd6dd137e
--- /dev/null
+++ b/drivers/cisco/dna_spaces/device_entry.cr
@@ -0,0 +1,26 @@
+require "./events"
+
+class Cisco::DNASpaces::DeviceEntry
+ include JSON::Serializable
+
+ getter device : Device
+ getter location : Location
+
+ @[JSON::Field(key: "visitId")]
+ getter visit_id : String
+
+ @[JSON::Field(key: "entryTimestamp")]
+ getter entry_timestamp : Int64
+
+ @[JSON::Field(key: "entryDateTime")]
+ getter entry_datetime : String
+
+ @[JSON::Field(key: "timeZone")]
+ getter time_zone : String
+
+ @[JSON::Field(key: "deviceClassification")]
+ getter device_classification : String
+
+ @[JSON::Field(key: "daysSinceLastVisit")]
+ getter days_sinc_last_visit : Int32
+end
diff --git a/drivers/cisco/dna_spaces/device_exit.cr b/drivers/cisco/dna_spaces/device_exit.cr
new file mode 100644
index 00000000000..151ef54cd5f
--- /dev/null
+++ b/drivers/cisco/dna_spaces/device_exit.cr
@@ -0,0 +1,38 @@
+require "./events"
+
+class Cisco::DNASpaces::DeviceExit
+ include JSON::Serializable
+
+ getter device : Device
+ getter location : Location
+
+ @[JSON::Field(key: "visitId")]
+ getter visit_id : String
+
+ @[JSON::Field(key: "visitDurationMinutes")]
+ getter visit_duration_minutes : Int32
+
+ @[JSON::Field(key: "visitDurationMinutes")]
+ getter visit_duration_minutes : Int32
+
+ @[JSON::Field(key: "entryTimestamp")]
+ getter entry_timestamp : Int64
+
+ @[JSON::Field(key: "entryDateTime")]
+ getter entry_datetime : String
+
+ @[JSON::Field(key: "exitTimestamp")]
+ getter exit_timestamp : Int64
+
+ @[JSON::Field(key: "exitDateTime")]
+ getter exit_datetime : String
+
+ @[JSON::Field(key: "timeZone")]
+ getter time_zone : String
+
+ @[JSON::Field(key: "deviceClassification")]
+ getter device_classification : String
+
+ @[JSON::Field(key: "visitClassification")]
+ getter visit_classification : String
+end
diff --git a/drivers/cisco/dna_spaces/device_location_update.cr b/drivers/cisco/dna_spaces/device_location_update.cr
new file mode 100644
index 00000000000..393017be928
--- /dev/null
+++ b/drivers/cisco/dna_spaces/device_location_update.cr
@@ -0,0 +1,51 @@
+require "./events"
+
+class Cisco::DNASpaces::DeviceLocationUpdate
+ include JSON::Serializable
+
+ getter device : Device
+ getter location : Location
+
+ getter ssid : String
+
+ @[JSON::Field(key: "rawUserId")]
+ getter raw_user_id : String
+
+ @[JSON::Field(key: "visitId")]
+ getter visit_id : String
+
+ @[JSON::Field(key: "lastSeen")]
+ property last_seen : Int64
+
+ @[JSON::Field(key: "deviceClassification")]
+ getter device_classification : String
+
+ @[JSON::Field(key: "mapId")]
+ property map_id : String
+
+ @[JSON::Field(key: "xPos")]
+ getter x_pos : Float64
+
+ @[JSON::Field(key: "yPos")]
+ getter y_pos : Float64
+
+ @[JSON::Field(key: "confidenceFactor")]
+ getter confidence_factor : Float64
+ getter latitude : Float64
+ getter longitude : Float64
+ getter unc : Float64
+
+ @[JSON::Field(ignore: true)]
+ @location_mappings : Hash(String, String)? = nil
+
+ # Ensure we only process these once
+ def location_mappings : Hash(String, String)
+ if mappings = @location_mappings
+ mappings
+ else
+ mappings = location.details
+ @location_mappings = mappings
+ mappings
+ end
+ end
+end
diff --git a/drivers/cisco/dna_spaces/device_presence.cr b/drivers/cisco/dna_spaces/device_presence.cr
new file mode 100644
index 00000000000..2c3973a4035
--- /dev/null
+++ b/drivers/cisco/dna_spaces/device_presence.cr
@@ -0,0 +1,54 @@
+require "./events"
+
+class Cisco::DNASpaces::DevicePresence
+ include JSON::Serializable
+
+ @[JSON::Field(key: "presenceEventType")]
+ getter presence_event_type : String
+
+ @[JSON::Field(key: "wasInActive")]
+ getter was_in_active : Bool
+ getter device : Device
+ getter location : Location
+
+ getter ssid : String
+
+ @[JSON::Field(key: "rawUserId")]
+ getter raw_user_id : String
+
+ @[JSON::Field(key: "visitId")]
+ getter visit_id : String
+
+ @[JSON::Field(key: "daysSinceLastVisit")]
+ getter days_since_last_visit : Int32
+
+ @[JSON::Field(key: "entryTimestamp")]
+ getter entry_timestamp : Int64
+
+ @[JSON::Field(key: "entryDateTime")]
+ getter entry_datetime : String
+
+ @[JSON::Field(key: "exitTimestamp")]
+ getter exit_timestamp : Int64
+
+ @[JSON::Field(key: "exitDateTime")]
+ getter exit_date_time : String
+
+ @[JSON::Field(key: "visitDurationMinutes")]
+ getter visit_duration_minutes : Int32
+
+ @[JSON::Field(key: "timeZone")]
+ getter time_zone : String
+
+ @[JSON::Field(key: "deviceClassification")]
+ getter device_classification : String
+
+ @[JSON::Field(key: "visitClassification")]
+ getter visit_classification : String
+
+ @[JSON::Field(key: "activeDevicesCount")]
+ getter active_devices_count : Int32
+
+ @[JSON::Field(key: "inActiveDevicesCount")]
+ getter inactive_devices_count : Int32
+end
diff --git a/drivers/cisco/dna_spaces/events.cr b/drivers/cisco/dna_spaces/events.cr
new file mode 100644
index 00000000000..db66a992585
--- /dev/null
+++ b/drivers/cisco/dna_spaces/events.cr
@@ -0,0 +1,118 @@
+require "json"
+require "../dna_spaces"
+require "./location"
+require "./device"
+require "./*"
+
+# This is used to map the various events into a simpler data structure
+abstract class Cisco::DNASpaces::Events
+ include JSON::Serializable
+
+ # event type hint
+ use_json_discriminator "eventType", {
+ "KEEP_ALIVE" => KeepAlive,
+ "DEVICE_ENTRY" => DeviceEntryWrapper,
+ "DEVICE_EXIT" => DeviceExitWrapper,
+ "PROFILE_UPDATE" => ProfileUpdateWrapper,
+ "LOCATION_CHANGE" => LocationChangeWrapper,
+ "DEVICE_LOCATION_UPDATE" => DeviceLocationUpdateWrapper,
+ "TP_PEOPLE_COUNT_UPDATE" => PeopleCountUpdateWrapper,
+ "DEVICE_PRESENCE" => DevicePresenceWrapper,
+ "USER_PRESENCE" => UserPresenceWrapper,
+ "APP_ACTIVATION" => AppActivactionWrapper,
+ "DEVICE_COUNT" => DeviceCountWrapper,
+ }
+
+ @[JSON::Field(key: "recordUid")]
+ getter record_uid : String
+
+ @[JSON::Field(key: "recordTimestamp")]
+ getter record_timestamp : Int64
+
+ @[JSON::Field(key: "spacesTenantId")]
+ getter spaces_tenant_id : String
+
+ @[JSON::Field(key: "spacesTenantName")]
+ getter spaces_tenant_name : String
+
+ @[JSON::Field(key: "partnerTenantId")]
+ getter partner_tenant_id : String
+end
+
+class Cisco::DNASpaces::KeepAlive < Cisco::DNASpaces::Events
+ getter eventType : String = "KEEP_ALIVE"
+
+ def payload
+ nil
+ end
+end
+
+class Cisco::DNASpaces::DeviceEntryWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "DEVICE_ENTRY"
+
+ @[JSON::Field(key: "deviceEntry")]
+ getter payload : DeviceEntry
+end
+
+class Cisco::DNASpaces::DeviceExitWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "DEVICE_EXIT"
+
+ @[JSON::Field(key: "deviceExit")]
+ getter payload : DeviceExit
+end
+
+class Cisco::DNASpaces::ProfileUpdateWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "PROFILE_UPDATE"
+
+ @[JSON::Field(key: "deviceProfileUpdate")]
+ getter payload : Device
+end
+
+class Cisco::DNASpaces::LocationChangeWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "LOCATION_CHANGE"
+
+ @[JSON::Field(key: "locationHierarchyChange")]
+ getter payload : LocationChange
+end
+
+class Cisco::DNASpaces::DeviceLocationUpdateWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "DEVICE_LOCATION_UPDATE"
+
+ @[JSON::Field(key: "deviceLocationUpdate")]
+ getter payload : DeviceLocationUpdate
+end
+
+class Cisco::DNASpaces::PeopleCountUpdateWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "TP_PEOPLE_COUNT_UPDATE"
+
+ @[JSON::Field(key: "tpPeopleCountUpdate")]
+ getter payload : PeopleCountUpdate
+end
+
+class Cisco::DNASpaces::DevicePresenceWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "DEVICE_PRESENCE"
+
+ @[JSON::Field(key: "devicePresence")]
+ getter payload : DevicePresence
+end
+
+class Cisco::DNASpaces::UserPresenceWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "USER_PRESENCE"
+
+ @[JSON::Field(key: "userPresence")]
+ getter payload : UserPresence
+end
+
+class Cisco::DNASpaces::AppActivactionWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "APP_ACTIVATION"
+
+ @[JSON::Field(key: "appActivation")]
+ getter payload : AppActivaction
+end
+
+class Cisco::DNASpaces::DeviceCountWrapper < Cisco::DNASpaces::Events
+ getter eventType : String = "DEVICE_COUNT"
+
+ @[JSON::Field(key: "deviceCounts")]
+ getter payload : DeviceCount
+end
diff --git a/drivers/cisco/dna_spaces/location.cr b/drivers/cisco/dna_spaces/location.cr
new file mode 100644
index 00000000000..12e2228beca
--- /dev/null
+++ b/drivers/cisco/dna_spaces/location.cr
@@ -0,0 +1,30 @@
+require "./events"
+
+class Cisco::DNASpaces::Location
+ include JSON::Serializable
+
+ @[JSON::Field(key: "locationId")]
+ getter location_id : String
+ getter name : String
+
+ # TODO:: this might be better as an enum
+ # if there are only limited types
+ @[JSON::Field(key: "inferredLocationTypes")]
+ getter tags : Array(String)
+
+ getter parent : Location?
+
+ # Maps tag names to location_ids
+ def details(mappings = {} of String => String)
+ parent.try &.details(mappings)
+ tags.each { |tag| mappings[tag] = location_id }
+ mappings
+ end
+
+ # Maps location_ids to location names
+ def descriptions(mappings = {} of String => String)
+ parent.try &.descriptions(mappings)
+ mappings[location_id] = name
+ mappings
+ end
+end
diff --git a/drivers/cisco/dna_spaces/location_change.cr b/drivers/cisco/dna_spaces/location_change.cr
new file mode 100644
index 00000000000..6043e172f14
--- /dev/null
+++ b/drivers/cisco/dna_spaces/location_change.cr
@@ -0,0 +1,32 @@
+require "./events"
+
+class Cisco::DNASpaces::LocationChange
+ include JSON::Serializable
+
+ @[JSON::Field(key: "changeType")]
+ getter change_type : String
+ getter location : Location
+
+ class Metadata
+ include JSON::Serializable
+
+ getter key : String
+ getter values : Array(String)
+ end
+
+ class LocationDetails
+ include JSON::Serializable
+
+ @[JSON::Field(key: "timeZone")]
+ getter time_zone : String
+ getter city : String
+ getter state : String
+ getter country : String
+ getter category : String
+
+ getter latitude : Float64
+ getter longitude : Float64
+
+ getter metadata : Array(Metadata)
+ end
+end
diff --git a/drivers/cisco/dna_spaces/location_details.cr b/drivers/cisco/dna_spaces/location_details.cr
new file mode 100644
index 00000000000..c69048af78f
--- /dev/null
+++ b/drivers/cisco/dna_spaces/location_details.cr
@@ -0,0 +1,16 @@
+require "./events"
+
+class Cisco::DNASpaces::LocationDetails
+ include JSON::Serializable
+
+ @[JSON::Field(key: "timeZone")]
+ getter time_zone : String
+
+ getter city : String
+ getter state : String
+ getter country : String
+ getter category : String
+
+ getter latitude : Float64
+ getter longitude : Float64
+end
diff --git a/drivers/cisco/dna_spaces/map_info.cr b/drivers/cisco/dna_spaces/map_info.cr
new file mode 100644
index 00000000000..31be13841d7
--- /dev/null
+++ b/drivers/cisco/dna_spaces/map_info.cr
@@ -0,0 +1,30 @@
+require "./events"
+
+class Cisco::DNASpaces::Dimension
+ include JSON::Serializable
+
+ getter length : Float64
+ getter width : Float64
+ getter height : Float64
+
+ @[JSON::Field(key: "offsetX")]
+ getter offset_x : Float64
+
+ @[JSON::Field(key: "offsetY")]
+ getter offset_y : Float64
+end
+
+class Cisco::DNASpaces::MapInfo
+ include JSON::Serializable
+
+ @[JSON::Field(key: "mapId")]
+ getter id : String
+
+ @[JSON::Field(key: "imageWidth")]
+ getter image_width : Float64
+
+ @[JSON::Field(key: "imageHeight")]
+ getter image_height : Float64
+
+ getter dimension : Cisco::DNASpaces::Dimension
+end
diff --git a/drivers/cisco/dna_spaces/people_count_update.cr b/drivers/cisco/dna_spaces/people_count_update.cr
new file mode 100644
index 00000000000..99d8f35cd22
--- /dev/null
+++ b/drivers/cisco/dna_spaces/people_count_update.cr
@@ -0,0 +1,32 @@
+require "./events"
+
+# This is triggered from telepresence devices
+class Cisco::DNASpaces::PeopleCountUpdate
+ include JSON::Serializable
+
+ @[JSON::Field(key: "tpDeviceId")]
+ getter tp_device_id : String
+ getter location : Location
+ getter presence : Bool
+
+ @[JSON::Field(key: "peopleCount")]
+ getter people_count : Int32
+
+ @[JSON::Field(key: "standbyState")]
+ getter standby_state : Int32
+
+ @[JSON::Field(key: "ambientNoise")]
+ getter ambient_noise : Int32
+
+ @[JSON::Field(key: "drynessScore")]
+ getter dryness_score : Int32
+
+ @[JSON::Field(key: "activeCalls")]
+ getter active_calls : Int32
+
+ @[JSON::Field(key: "presentationState")]
+ getter presentation_state : Int32
+
+ @[JSON::Field(key: "timeStamp")]
+ getter timestamp : Int64
+end
diff --git a/drivers/cisco/dna_spaces/user_presence.cr b/drivers/cisco/dna_spaces/user_presence.cr
new file mode 100644
index 00000000000..1882f4b25a2
--- /dev/null
+++ b/drivers/cisco/dna_spaces/user_presence.cr
@@ -0,0 +1,83 @@
+require "./events"
+
+class Cisco::DNASpaces::UserPresence
+ include JSON::Serializable
+
+ class User
+ include JSON::Serializable
+
+ @[JSON::Field(key: "userId")]
+ getter user_id : String
+
+ @[JSON::Field(key: "deviceIds")]
+ getter device_ids : Array(String)
+ getter tags : Array(String)
+ getter mobile : String
+ getter email : String
+ getter gender : String
+
+ @[JSON::Field(key: "firstName")]
+ getter first_name : String
+
+ @[JSON::Field(key: "lastName")]
+ getter last_name : String
+
+ @[JSON::Field(key: "postalCode")]
+ getter postal_code : String
+
+ # otherFields
+ # socialNetworkInfo
+ end
+
+ class UserCount
+ include JSON::Serializable
+
+ @[JSON::Field(key: "usersWithUserId")]
+ getter users_with_user_id : Int64
+
+ @[JSON::Field(key: "usersWithoutUserId")]
+ getter users_without_user_id : Int64
+
+ @[JSON::Field(key: "totalUsers")]
+ getter total_users : Int64
+ end
+
+ @[JSON::Field(key: "presenceEventType")]
+ getter presence_event_type : String
+
+ @[JSON::Field(key: "wasInActive")]
+ getter was_in_active : Bool
+
+ getter user : User
+ getter location : Location
+
+ @[JSON::Field(key: "rawUserId")]
+ getter raw_user_id : String
+
+ @[JSON::Field(key: "visitId")]
+ getter visit_id : String
+
+ @[JSON::Field(key: "entryTimestamp")]
+ getter entry_timestamp : Int64
+
+ @[JSON::Field(key: "entryDateTime")]
+ getter entry_datetime : String
+
+ @[JSON::Field(key: "exitTimestamp")]
+ getter exit_timestamp : Int64
+
+ @[JSON::Field(key: "exitDateTime")]
+ getter exit_datetime : String
+
+ @[JSON::Field(key: "visitDurationMinutes")]
+ getter visit_duration_minutes : Int32
+
+ @[JSON::Field(key: "timeZone")]
+ getter time_zone : String
+
+ @[JSON::Field(key: "activeUsersCount")]
+ getter active_users_count : UserCount
+
+ @[JSON::Field(key: "inActiveUsersCount")]
+ getter inactive_users_count : UserCount
+end
diff --git a/drivers/cisco/dna_spaces_spec.cr b/drivers/cisco/dna_spaces_spec.cr
new file mode 100644
index 00000000000..c3df6cba22c
--- /dev/null
+++ b/drivers/cisco/dna_spaces_spec.cr
@@ -0,0 +1,16 @@
+DriverSpecs.mock_driver "Cisco::DNASpaces" do
+ # The dashboard should request the streaming API
+ expect_http_request do |request, response|
+ headers = request.headers
+ if headers["X-API-KEY"]? == "X-API-KEY"
+ response.headers["Transfer-Encoding"] = "chunked"
+ response.status_code = 200
+ response << %({"recordUid":"event-85b84f15","recordTimestamp":1605502585236,"spacesTenantId":"","spacesTenantName":"","partnerTenantId":"","eventType":"KEEP_ALIVE"})
+ else
+ response.status_code = 401
+ end
+ end
+
+ # Should standardise the format of MAC addresses
+ exec(:format_mac, "0x12:34:A6-789B").get.should eq %(1234a6789b)
+end
diff --git a/drivers/cisco/meraki/captive_portal.cr b/drivers/cisco/meraki/captive_portal.cr
new file mode 100644
index 00000000000..5866d8d181b
--- /dev/null
+++ b/drivers/cisco/meraki/captive_portal.cr
@@ -0,0 +1,145 @@
+module Cisco; end
+
+module Cisco::Meraki; end
+
+require "json"
+require "openssl"
+
+class Cisco::Meraki::CaptivePortal < PlaceOS::Driver
+ # Discovery Information
+ descriptive_name "Cisco Meraki Captive Portal"
+ generic_name :CaptivePortal
+ description %(
+ for more information visit: https://meraki.cisco.com/lib/pdf/meraki_whitepaper_captive_portal.pdf
+ )
+
+ default_settings({
+ wifi_secret: "anything really",
+ default_timezone: "Australia/Sydney",
+ date_format: "%Y%m%d",
+ # duration of access in hours
+ access_duration: 12,
+ # Length of the clients wifi code
+ code_length: 4,
+ success_url: "https://company.com/welcome",
+ })
+
+ def on_load
+ on_update
+ end
+
+ @wifi_secret : String = ""
+ @date_format : String = "%Y%m%d"
+ @success_url : String = "https://place.technology/"
+ @default_timezone : Time::Location = Time::Location.load("Australia/Sydney")
+ @access_duration : Time::Span = 12.hours
+ @code_length : Int32 = 4
+
+ @denied : UInt64 = 0_u64
+ @granted : UInt64 = 0_u64
+ @errors : UInt64 = 0_u64
+
+ @guests : Hash(String, ChallengePayload) = {} of String => ChallengePayload
+
+ def on_update
+ @wifi_secret = setting?(String, :wifi_secret) || "anything really"
+ @date_format = setting?(String, :date_format) || "%Y%m%d"
+ @success_url = setting?(String, :success_url) || "https://place.technology/"
+ @access_duration = (setting?(Int32, :access_duration) || 12).hours
+ @code_length = setting?(Int32, :code_length) || 4
+
+ time_zone = setting?(String, :default_timezone).presence
+ @default_timezone = Time::Location.load(time_zone) if time_zone
+ end
+
+ @[Security(Level::Support)]
+ def guests
+ @guests
+ end
+
+ @[Security(Level::Support)]
+ def lookup(mac : String)
+ @guests[format_mac(mac)]
+ end
+
+ def generate_guest_data(email : String, time : Int64, time_zone : String? = nil)
+ time_zone = time_zone.presence ? Time::Location.load(time_zone.not_nil!) : @default_timezone
+ date = Time.unix(time).in(time_zone).to_s(@date_format)
+ guest_string = "#{email.downcase}-#{date}-#{@wifi_secret}"
+
+ OpenSSL::Digest.new("SHA256").update(guest_string).final.hexstring
+ end
+
+ # Splits the SHA256 into code length and then randomly selects one
+ def generate_guest_token(email : String, time : Int64, time_zone : String? = nil)
+ generate_guest_data(email, time, time_zone).scan(/.{#{@code_length}}/).sample(1)[0][0]
+ end
+
+ class ChallengePayload
+ include JSON::Serializable
+
+ property ap_mac : String
+ property client_ip : String
+ property client_mac : String
+ property base_grant_url : String
+ property user_continue : String?
+
+ # key they were provided in their invite email
+ property code : String
+ property email : String
+ property timezone : String?
+
+ property expires : Time? = nil
+ end
+
+ EMPTY_HEADERS = {} of String => String
+ JSON_HEADERS = {
+ "Content-Type" => "application/json",
+ }
+
+ # Webhook for providing guest access
+ def challenge(method : String, headers : Hash(String, Array(String)), body : String)
+ logger.debug { "guest access attempt: #{method},\nheaders #{headers},\nbody #{body}" }
+
+ challenge = ChallengePayload.from_json(body)
+
+ check_code = challenge.code
+ guest_codes = generate_guest_data(challenge.email, Time.utc.to_unix, challenge.timezone)
+ matched = guest_codes.scan(/.{#{@code_length}}/).count { |code| code[0] == check_code } > 0
+
+ if matched
+ challenge.expires = @access_duration.from_now
+ @guests[format_mac(challenge.client_mac)] = challenge
+ @granted += 1_u64
+ self[:granted_access] = @granted
+
+ redirect_url = "#{challenge.base_grant_url}?duration=#{@access_duration.to_i}&continue_url=#{challenge.user_continue || @success_url}"
+ response = {
+ redirect_to: redirect_url,
+ }.to_json
+
+ logger.debug { "successful joined network #{challenge.inspect}" }
+
+ # Redirect to the success URL
+ {HTTP::Status::OK, JSON_HEADERS, response}
+ else
+ @denied += 1_u64
+ self[:denied_access] = @denied
+
+ logger.debug { "failed wifi access attempt by #{challenge.inspect}" }
+
+ {HTTP::Status::NOT_ACCEPTABLE, JSON_HEADERS, "{}"}
+ end
+ rescue error
+ @errors += 1_u64
+ self[:errors] = @errors
+ last_error = error.inspect_with_backtrace
+ self[:last_error] = last_error
+ logger.error { "failed to parse wifi challenge payload\n#{error}" }
+ {HTTP::Status::INTERNAL_SERVER_ERROR, EMPTY_HEADERS, nil}
+ end
+
+ protected def format_mac(address : String)
+ address.gsub(/(0x|[^0-9A-Fa-f])*/, "").downcase
+ end
+end
diff --git a/drivers/cisco/meraki/captive_portal_spec.cr b/drivers/cisco/meraki/captive_portal_spec.cr
new file mode 100644
index 00000000000..a627b8b4b81
--- /dev/null
+++ b/drivers/cisco/meraki/captive_portal_spec.cr
@@ -0,0 +1,15 @@
+require "openssl"
+
+DriverSpecs.mock_driver "Cisco::Meraki::CaptivePortal" do
+ date = Time.unix(1599477274).in(Time::Location.load("Australia/Sydney")).to_s("%Y%m%d")
+ hexdigest = OpenSSL::Digest.new("SHA256").update("guest@email.com-#{date}-anything really").final.hexstring
+
+ # Check the hex codes match
+ retval = exec(:generate_guest_data, "guest@email.com", 1599477274, "Australia/Sydney")
+ retval.get.should eq hexdigest
+
+ # check it matches on of the codes
+ codes = hexdigest.scan(/.{4}/).map { |code| code[0] }
+ retval = exec(:generate_guest_token, "guest@email.com", 1599477274, "Australia/Sydney")
+ codes.includes?(retval.get.not_nil!.as_s).should eq true
+end
diff --git a/drivers/cisco/meraki/dashboard.cr b/drivers/cisco/meraki/dashboard.cr
new file mode 100644
index 00000000000..fe58ea7dc64
--- /dev/null
+++ b/drivers/cisco/meraki/dashboard.cr
@@ -0,0 +1,173 @@
+require "uri"
+require "json"
+require "link-header"
+require "./scanning_api"
+
+class Cisco::Meraki::Dashboard < PlaceOS::Driver
+ # Discovery Information
+ descriptive_name "Cisco Meraki Dashboard"
+ generic_name :Dashboard
+ uri_base "https://api.meraki.com"
+ description %(
+ for more information visit:
+ * Dashboard API: https://documentation.meraki.com/zGeneral_Administration/Other_Topics/The_Cisco_Meraki_Dashboard_API
+ * Scanning API: https://developer.cisco.com/meraki/scanning-api/#!introduction/scanning-api
+
+ NOTE:: API Call volume is rate limited to 5 calls per second per organization
+ )
+
+ default_settings({
+ meraki_validator: "configure if scanning API is enabled",
+ meraki_secret: "configure if scanning API is enabled",
+ meraki_api_key: "configure for the dashboard API",
+
+ # Max requests a second made to the dashboard
+ rate_limit: 4,
+ debug_payload: false,
+ })
+
+ def on_load
+ spawn { rate_limiter }
+ on_update
+ end
+
+ @scanning_validator : String = ""
+ @scanning_secret : String = ""
+ @api_key : String = ""
+
+ @rate_limit : Int32 = 4
+ @channel : Channel(Nil) = Channel(Nil).new(1)
+ @queue_lock : Mutex = Mutex.new
+ @queue_size = 0
+ @wait_time : Time::Span = 300.milliseconds
+
+ @debug_payload : Bool = false
+
+ def on_update
+ @scanning_validator = setting?(String, :meraki_validator) || ""
+ @scanning_secret = setting?(String, :meraki_secret) || ""
+ @api_key = setting?(String, :meraki_api_key) || ""
+
+ @rate_limit = setting?(Int32, :rate_limit) || 4
+ @wait_time = 1.second / @rate_limit
+
+ @debug_payload = setting?(Bool, :debug_payload) || false
+ end
+
+ # Perform fetch with the required API request limits in place
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def fetch(location : String)
+ req(location, &.body)
+ end
+
+ protected def req(location : String)
+ if (@wait_time * @queue_size) > 10.seconds
+ raise "wait time would be exceeded for API request, #{@queue_size} requests already queued"
+ end
+
+ @queue_lock.synchronize { @queue_size += 1 }
+ @channel.receive
+ @queue_lock.synchronize { @queue_size -= 1 }
+
+ headers = HTTP::Headers{
+ "X-Cisco-Meraki-API-Key" => @api_key,
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ "User-Agent" => "PlaceOS/2.0 PlaceTechnology",
+ }
+
+ uri = URI.parse(location)
+ response = if uri.host.nil?
+ get(location, headers: headers)
+ else
+ HTTP::Client.get(location, headers: headers)
+ end
+
+ if response.success?
+ yield response
+ elsif response.status.found?
+ # Meraki might return a `302` on GET requests
+ response = HTTP::Client.get(response.headers["Location"], headers: headers)
+ if response.success?
+ yield response
+ else
+ raise "request #{location} failed with status: #{response.status_code}"
+ end
+ else
+ raise "request #{location} failed with status: #{response.status_code}"
+ end
+ end
+
+ EMPTY_HEADERS = {} of String => String
+ SUCCESS_RESPONSE = {HTTP::Status::OK, EMPTY_HEADERS, nil}
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def poll_clients(network_id : String? = nil, timespan : UInt32 = 900_u32)
+ clients = [] of Client
+ next_page = "/api/v1/networks/#{network_id}/clients?perPage=1000×pan=#{timespan}"
+
+ loop do
+ break unless next_page
+
+ next_page = req(next_page) do |response|
+ clients.concat Array(Client).from_json(response.body)
+ LinkHeader.new(response)["next"]?
+ end
+ end
+
+ clients
+ end
+
+ # Webhook endpoint for scanning API, expects version 3
+ def scanning_api(method : String, headers : Hash(String, Array(String)), body : String)
+ logger.debug { "scanning API received: #{method},\nheaders #{headers},\nbody size #{body.size}" }
+ logger.debug { body } if @debug_payload
+
+ # Return the scanning API validator code on a GET request
+ return {HTTP::Status::OK.to_i, EMPTY_HEADERS, @scanning_validator} if method == "GET"
+
+ # Check the version matches
+ if !body.starts_with?(%({"version":"3.0"))
+ logger.warn { "unknown scanning API message received:\n#{body[0..96]}" }
+ return SUCCESS_RESPONSE
+ end
+
+ # Parse the data posted
+ begin
+ seen = DevicesSeen.from_json(body)
+ logger.debug { "parsed meraki payload" }
+
+ # We're only interested in Wifi at the moment
+ if seen.message_type != "WiFi"
+ logger.debug { "ignoring message type: #{seen.message_type}" }
+ return SUCCESS_RESPONSE
+ end
+
+ # Check the secret matches
+ raise "secret mismatch, sent: #{seen.secret}" unless seen.secret == @scanning_secret
+
+ self[seen.data.network_id] = seen.data.observations
+ rescue e
+ logger.error { "failed to parse meraki scanning API payload\n#{e.inspect_with_backtrace}" }
+ logger.debug { "failed payload body was\n#{body}" }
+ end
+
+ # Return a 200 response
+ SUCCESS_RESPONSE
+ end
+
+ protected def rate_limiter
+ loop do
+ begin
+ @channel.send(nil)
+ rescue error
+ logger.error(exception: error) { "issue with rate limiter" }
+ ensure
+ sleep @wait_time
+ end
+ end
+ rescue
+ # Possible error with logging exception, restart rate limiter silently
+ spawn { rate_limiter }
+ end
+end
diff --git a/drivers/cisco/meraki/dashboard_spec.cr b/drivers/cisco/meraki/dashboard_spec.cr
new file mode 100644
index 00000000000..30a7ae2a2d9
--- /dev/null
+++ b/drivers/cisco/meraki/dashboard_spec.cr
@@ -0,0 +1,20 @@
+require "./scanning_api"
+
+DriverSpecs.mock_driver "Cisco::Meraki::Dashboard" do
+ # Send the request
+ retval = exec(:fetch, "/api/v0/organizations")
+
+ # The dashboard should send a HTTP request with the API key
+ expect_http_request do |request, response|
+ headers = request.headers
+ if headers["X-Cisco-Meraki-API-Key"]? == "configure for the dashboard API"
+ response.status_code = 202
+ response << %([{"id":"org id","name":"place tech"}])
+ else
+ response.status_code = 401
+ end
+ end
+
+ # Should return the payload
+ retval.get.should eq %([{"id":"org id","name":"place tech"}])
+end
diff --git a/drivers/cisco/meraki/geo.cr b/drivers/cisco/meraki/geo.cr
new file mode 100644
index 00000000000..cf2c419721c
--- /dev/null
+++ b/drivers/cisco/meraki/geo.cr
@@ -0,0 +1,73 @@
+require "math"
+require "json"
+
+module Cisco; end
+
+module Cisco::Meraki; end
+
+module Cisco::Meraki::Geo
+ struct Point
+ include JSON::Serializable
+
+ def initialize(@lat, @lng)
+ end
+
+ property lat : Float64
+ property lng : Float64
+ end
+
+ struct Distance
+ include JSON::Serializable
+
+ def initialize(@x, @y)
+ end
+
+ property x : Float64
+ property y : Float64
+ end
+
+ def self.calculate_xy(top_left : Point, bottom_left : Point, bottom_right : Point, position, distance : Distance)
+ y_base = geo_distance(top_left, bottom_left)
+ a = geo_distance(top_left, position)
+ c = geo_distance(bottom_left, position)
+ x_raw = triangle_height(a, y_base, c)
+
+ x_base = geo_distance(bottom_left, bottom_right)
+ a = geo_distance(bottom_left, position)
+ c = geo_distance(bottom_right, position)
+ y_raw = triangle_height(a, x_base, c)
+
+ # find the percentage distance from the origin
+ percentage_height = y_raw / y_base
+ percentage_width = x_raw / x_base
+
+ # adjust into range provided by the original distances
+ Distance.new(distance.x * percentage_width, distance.y * percentage_height)
+ end
+
+ # radius in meters, approx as we're using a perfect sphere the same volume as the earth
+ EarthRadiusApprox = 6371000.7900_f64
+ Radians = Math::PI / 180_f64
+
+ # https://www.movable-type.co.uk/scripts/latlong.html
+ # returns the distance in meters
+ def self.geo_distance(start : Point, ending)
+ lat_diff = (ending.lat - start.lat) * Radians
+ lng_diff = (ending.lng - start.lng) * Radians
+ start_lat_radian = start.lat * Radians
+ end_lng_radian = ending.lng * Radians
+
+ a = Math.sin(lat_diff / 2_f64) * Math.sin(lat_diff / 2_f64) +
+ Math.cos(start_lat_radian) * Math.cos(end_lng_radian) *
+ Math.sin(lng_diff / 2_f64) * Math.sin(lng_diff / 2_f64)
+
+ c = 2_f64 * Math.atan2(Math.sqrt(a), Math.sqrt(1_f64 - a))
+
+ EarthRadiusApprox * c
+ end
+
+ # https://www.omnicalculator.com/math/triangle-height
+ def self.triangle_height(a : Float64, base : Float64, c : Float64)
+ 0.5_f64 * Math.sqrt((a + base + c) * (base + c - a) * (a - base + c) * (a + base - c)) / base
+ end
+end
diff --git a/drivers/cisco/meraki/meraki_locations.cr b/drivers/cisco/meraki/meraki_locations.cr
new file mode 100644
index 00000000000..a93b794e702
--- /dev/null
+++ b/drivers/cisco/meraki/meraki_locations.cr
@@ -0,0 +1,741 @@
+require "json"
+require "s2_cells"
+require "./scanning_api"
+require "placeos-driver/interface/locatable"
+
+class Cisco::Meraki::Locations < PlaceOS::Driver
+ include Interface::Locatable
+
+ # Discovery Information
+ descriptive_name "Meraki Location Service"
+ generic_name :MerakiLocations
+
+ description %(requires meraki dashboard driver for API calls)
+
+ accessor dashboard : Dashboard_1
+
+ default_settings({
+ # We will always accept a reading with a confidence lower than this
+ acceptable_confidence: 5.0,
+
+ # Max Uncertainty in meters - we don't accept positions that are less certain
+ maximum_uncertainty: 25.0,
+
+ # can we use the meraki dashboard API for user lookups
+ default_network_id: "network_id",
+
+ # Area index each point on a floor lands on
+ # 21 == ~4 meters squared, which given wifi variance is good enough for tracing
+ # S2 cell levels: https://s2geometry.io/resources/s2cell_statistics.html
+ s2_level: 21,
+ debug_payload: false,
+ debug_webhook: false,
+
+ # Level mappings, level name for human readability
+ floorplan_mappings: {
+ "g_727894289773756672" => {
+ "building": "zone-12345",
+ "level": "zone-123456",
+ "level_name": "BUILDING - L1",
+ },
+ },
+
+ # Time before a user location is considered probably too old
+ max_location_age: 10,
+
+ # Ignore certain usernames from the dashboard
+ ignore_usernames: ["host/"],
+
+ # Enable / Disable dashboard username lookup completely
+ disable_username_lookup: false,
+ })
+
+ def on_load
+ # We want to store our user => mac_address mappings in redis
+ @user_mac_mappings = PlaceOS::Driver::RedisStorage.new(module_id, "user_macs")
+ on_update
+ end
+
+ @acceptable_confidence : Float64 = 5.0
+ @maximum_uncertainty : Float64 = 25.0
+
+ @time_multiplier : Float64 = 0.0
+ @confidence_multiplier : Float64 = 0.0
+ @max_location_age : Time::Span = 6.minutes
+ @drift_location_age : Time::Span = 4.minutes
+ @confidence_time : Time::Span = 2.minutes
+
+ @storage_lock : Mutex = Mutex.new
+ @user_mac_mappings : PlaceOS::Driver::RedisStorage? = nil
+ @default_network : String = ""
+ @floorplan_mappings : Hash(String, Hash(String, String | Float64)) = Hash(String, Hash(String, String | Float64)).new
+ @floorplan_sizes = {} of String => FloorPlan
+ @network_devices = {} of String => NetworkDevice
+
+ @s2_level : Int32 = 21
+ @ignore_usernames : Array(String) = [] of String
+
+ @debug_payload : Bool = false
+ @debug_webhook : Bool = false
+
+ def on_update
+ @default_network = setting?(String, :default_network_id) || ""
+
+ @acceptable_confidence = setting?(Float64, :acceptable_confidence) || 5.0
+ @maximum_uncertainty = setting?(Float64, :maximum_uncertainty) || 25.0
+
+ @max_location_age = (setting?(UInt32, :max_location_age) || 6).minutes
+ # Age we keep a confident value (without drifting towards less confidence)
+ @confidence_time = @max_location_age / 3
+ # Age at which we discard a drifting value (accepting a less confident value)
+ @drift_location_age = @max_location_age - @confidence_time
+
+ # How much confidence do we have in this new value, relative to an old confident value
+ @time_multiplier = 1.0_f64 / (@drift_location_age.to_i - @confidence_time.to_i).to_f64
+ @confidence_multiplier = 1.0_f64 / (@maximum_uncertainty.to_i - @acceptable_confidence.to_i).to_f64
+
+ @floorplan_mappings = setting?(Hash(String, Hash(String, String | Float64)), :floorplan_mappings) || @floorplan_mappings
+
+ @s2_level = setting?(Int32, :s2_level) || 21
+ @debug_payload = setting?(Bool, :debug_payload) || false
+ @debug_webhook = setting?(Bool, :debug_webhook) || false
+ @ignore_usernames = setting?(Array(String), :ignore_usernames) || [] of String
+ disable_username_lookup = setting?(Bool, :disable_username_lookup) || false
+
+ schedule.clear
+ if @default_network.presence
+ schedule.every(2.minutes) { map_users_to_macs } unless disable_username_lookup
+ schedule.every(29.minutes) { sync_floorplan_sizes }
+ schedule.in(30.milliseconds) { sync_floorplan_sizes }
+ end
+ schedule.every(30.minutes) { cleanup_caches }
+
+ subscriptions.clear
+ if @default_network.presence
+ dashboard.subscribe(@default_network) do |_subscription, new_value|
+ # values are always raw JSON strings
+ parse_new_locations(new_value)
+ end
+ end
+ end
+
+ protected def user_mac_mappings
+ @storage_lock.synchronize {
+ yield @user_mac_mappings.not_nil!
+ }
+ end
+
+ protected def req(location : String)
+ yield dashboard.fetch(location).get.as_s
+ end
+
+ struct Lookup
+ include JSON::Serializable
+
+ property time : Time
+ property mac : String
+
+ def initialize(@time, @mac)
+ end
+ end
+
+ # MAC Address => Location
+ @locations : Hash(String, Location) = {} of String => Location
+ @ip_lookup : Hash(String, Lookup) = {} of String => Lookup
+
+ def lookup_ip(address : String)
+ @ip_lookup[address.downcase]?
+ end
+
+ def locate_mac(address : String)
+ @locations[format_mac(address)]?
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def inspect_foorplans
+ @floorplan_sizes
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def inspect_network_devices
+ @network_devices
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def inspect_state
+ logger.debug {
+ "IP Mappings: #{@ip_lookup.keys}\n\nMAC Locations: #{@locations.keys}\n\nClient Details: #{@client_details.keys}"
+ }
+ {ip_mappings: @ip_lookup.size, tracking: @locations.size, client_details: @client_details.size}
+ end
+
+ # Returns the list of users who can be located
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def locateable
+ too_old = location_max_age = @max_location_age.ago
+ @client_details.compact_map do |mac, client|
+ location = @locations[mac]?
+ client.user if location && ((location.time > too_old) || (client.time_added > too_old))
+ end
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def poll_clients(network_id : String? = nil, timespan : UInt32 = 900_u32)
+ network_id = network_id.presence || @default_network
+ Array(Client).from_json dashboard.poll_clients(network_id, timespan).get.to_json
+ end
+
+ @client_details : Hash(String, Client) = {} of String => Client
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def map_users_to_macs(network_id : String? = nil)
+ network_id = network_id.presence || @default_network
+
+ logger.debug { "mapping users to device MACs" }
+ clients = poll_clients(network_id)
+
+ new_devices = 0
+ updated_dev = 0
+ now = Time.utc
+
+ logger.debug { "mapping found #{clients.size} devices" }
+
+ user_mac_mappings do |storage|
+ clients.each do |client|
+ # So we can merge additional details into device location responses
+ user_mac = format_mac(client.mac)
+ client.time_added = now
+
+ user_id = client.user
+
+ if user_id
+ @ignore_usernames.each do |name|
+ if user_id.starts_with?(name)
+ client.user = user_id = nil
+ break
+ end
+ end
+ end
+
+ # Attempt to lookup username via learning
+ if user_id.nil?
+ if known_id = storage[user_mac]?
+ client.user = known_id
+ end
+ end
+
+ @client_details[user_mac] = client
+ next unless user_id
+
+ was_update, was_new = map_user_mac(user_mac, user_id, storage)
+ updated_dev += 1 if was_update
+ new_devices += 1 if was_new
+ end
+ end
+
+ logger.debug { "mapping assigned #{new_devices} new devices, #{updated_dev} user updated" }
+ nil
+ end
+
+ protected def map_user_mac(user_mac, user_id, storage)
+ updated_dev = false
+ new_devices = false
+ user_id = format_username(user_id)
+
+ # Check if mac mapping already exists
+ existing_user = storage[user_mac]?
+ return {false, false} if existing_user == user_id
+
+ # Remove any pervious mappings
+ if existing_user
+ updated_dev = true
+ if user_macs = storage[existing_user]?
+ macs = Array(String).from_json(user_macs)
+ macs.delete(user_mac)
+ storage[existing_user] = macs.to_json
+ end
+ else
+ new_devices = true
+ end
+
+ # Update the user mappings
+ storage[user_mac] = user_id
+ macs = if user_macs = storage[user_id]?
+ tmp_macs = Array(String).from_json(user_macs)
+ tmp_macs.unshift(user_mac)
+ tmp_macs.uniq!
+ tmp_macs[0...9]
+ else
+ [user_mac]
+ end
+ storage[user_id] = macs
+
+ {updated_dev, new_devices}
+ end
+
+ def format_username(user : String)
+ if user.includes? "@"
+ user = user.split("@")[0]
+ elsif user.includes? "\\"
+ user = user.split("\\")[1]
+ end
+ user.downcase
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ username = format_username(username.presence || email.presence.not_nil!)
+ if macs = user_mac_mappings { |s| s[username]? }
+ Array(String).from_json(macs)
+ else
+ [] of String
+ end
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ lookup = format_mac(mac_address)
+ if user = user_mac_mappings { |s| s[lookup]? }
+ {
+ location: "wireless",
+ assigned_to: user,
+ mac_address: lookup,
+ }
+ end
+ end
+
+ # returns locations based on most recently seen
+ # versus most accurate location
+ def locate_user(email : String? = nil, username : String? = nil)
+ username = format_username(username.presence || email.presence.not_nil!)
+
+ if macs = user_mac_mappings { |s| s[username]? }
+ location_max_age = @max_location_age.ago
+
+ Array(String).from_json(macs).compact_map { |mac|
+ if location = locate_mac(mac)
+ client = @client_details[mac]?
+
+ # We set these here to speed up processing
+ location.client = client
+ location.mac = mac
+
+ if client && client.time_added > location_max_age
+ location
+ elsif location.time > location_max_age
+ location
+ end
+ end
+ }.sort! { |a, b|
+ b.time <=> a.time
+ }.map { |location|
+ lat = location.lat
+ lon = location.lng
+
+ loc = {
+ "location" => "wireless",
+ "coordinates_from" => "bottom-left",
+ "x" => location.x,
+ "y" => location.y,
+ "lon" => lon,
+ "lat" => lat,
+ "s2_cell_id" => lat ? S2Cells::LatLon.new(lat.not_nil!, lon.not_nil!).to_token(@s2_level) : nil,
+ "mac" => location.mac,
+ "variance" => location.variance,
+ "last_seen" => location.time.to_unix,
+ "meraki_floor_id" => location.floor_plan_id,
+ "meraki_floor_name" => location.floor_plan_name,
+ }
+
+ # Add our zone IDs to the response
+ if level_data = @floorplan_mappings[location.floor_plan_id]?
+ level_data.each { |k, v| loc[k] = v }
+ end
+
+ # Add meraki map information to the response
+ if map_size = @floorplan_sizes[location.floor_plan_id]?
+ loc["map_width"] = map_size.width
+ loc["map_height"] = map_size.height
+ end
+
+ # Add additional client information if it's available
+ if client = location.client
+ loc["manufacturer"] = client.manufacturer if client.manufacturer
+ loc["os"] = client.os if client.os
+ loc["ssid"] = client.ssid if client.ssid
+ end
+
+ loc
+ }
+ else
+ [] of Nil
+ end
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "looking up device locations in #{zone_id}" }
+ return [] of String if location.presence && location != "wireless"
+
+ # Find the floors associated with the provided zone id
+ floors = [] of String
+ @floorplan_mappings.each do |floor_id, data|
+ floors << floor_id if data.values.includes?(zone_id)
+ end
+ logger.debug { "found matching meraki floors: #{floors}" }
+ return [] of String if floors.empty?
+
+ checking_count = @locations.size
+ wrong_floor = 0
+ too_old = 0
+
+ # Find the devices that are on the matching floors
+ oldest_location = @max_location_age.ago
+ matching = @locations.compact_map do |mac, loc|
+ # We set this here to speed up processing
+ client = @client_details[mac]?
+ loc.client = client
+
+ if loc.time < oldest_location
+ if client
+ if client.time_added < oldest_location
+ too_old += 1
+ next
+ end
+ else
+ too_old += 1
+ next
+ end
+ end
+ if !floors.includes?(loc.floor_plan_id)
+ wrong_floor += 1
+ next
+ end
+ # ensure the formatted mac is being used
+ loc.mac = mac
+ loc
+ end
+
+ logger.debug { "found #{matching.size} matching devices\nchecked #{checking_count} locations, #{wrong_floor} were on the wrong floor, #{too_old} were too old" }
+
+ # Build the payload on the matching locations
+ matching.group_by(&.floor_plan_id).flat_map { |floor_id, locations|
+ map_width = -1.0
+ map_height = -1.0
+
+ if map_size = @floorplan_sizes[floor_id]?
+ map_width = map_size.width
+ map_height = map_size.height
+ elsif mappings = @floorplan_mappings[floor_id]?
+ map_width = (mappings["width"]? || map_width).as(Float64)
+ map_height = (mappings["height"]? || map_width).as(Float64)
+ end
+
+ locations.compact_map do |loc|
+ lat = loc.lat
+ lon = loc.lng
+
+ # Add additional client information if it's available
+ if client = @client_details[loc.mac]?
+ manufacturer = client.manufacturer
+ os = client.os
+ ssid = client.ssid
+ end
+
+ # Skip payloads with invalid coordinates
+ if (x = loc.x) && (y = loc.y)
+ if x.is_a?(Float64) && y.is_a?(Float64)
+ if loc.x.as(Float64).nan? || loc.y.as(Float64).nan?
+ logger.warn { "ignoring bad location for #{loc.mac}, NaN" }
+ next
+ end
+ else
+ logger.warn { "ignoring bad location for #{loc.mac}, unexpected value #{loc.x.inspect}" }
+ next
+ end
+ else
+ logger.warn { "ignoring bad location for #{loc.mac}, no coordinates provided" }
+ next
+ end
+
+ {
+ location: :wireless,
+ coordinates_from: "bottom-left",
+ x: loc.x,
+ y: loc.y,
+ lon: lon,
+ lat: lat,
+ s2_cell_id: lat ? S2Cells::LatLon.new(lat.not_nil!, lon.not_nil!).to_token(@s2_level) : nil,
+ mac: loc.mac,
+ variance: loc.variance,
+ last_seen: loc.time.to_unix,
+ map_width: map_width,
+ map_height: map_height,
+ manufacturer: manufacturer,
+ os: os,
+ ssid: ssid,
+ }
+ end
+ }
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def cleanup_caches : Nil
+ logger.debug { "removing IP and location data that is over 30 minutes old" }
+
+ # IP => MAC mappings
+ old = 30.minutes.ago
+ remove_keys = [] of String
+ @ip_lookup.each { |ip, lookup| remove_keys << ip if lookup.time < old }
+ remove_keys.each { |ip| @ip_lookup.delete(ip) }
+ logger.debug { "removed #{remove_keys.size} IP => MAC mappings" }
+
+ # IP => Username mappings
+ remove_keys.clear
+ @ip_usernames.each { |ip, lookup| remove_keys << ip if lookup.time < old }
+ remove_keys.each { |ip| @ip_usernames.delete(ip) }
+ logger.debug { "removed #{remove_keys.size} IP => Username mappings" }
+
+ # Client details
+ remove_keys.clear
+ @client_details.each { |mac, client| remove_keys << mac if client.time_added < old }
+ remove_keys.each { |mac| @client_details.delete(mac) }
+ logger.debug { "removed #{remove_keys.size} client details" }
+
+ # MACs
+ remove_keys.clear
+ @locations.each do |mac, location|
+ if location.time < old
+ if client = @client_details[mac]?
+ remove_keys << mac if client.time_added < old
+ else
+ remove_keys << mac
+ end
+ end
+ end
+ remove_keys.each { |mac| @locations.delete(mac) }
+ logger.debug { "removed #{remove_keys.size} MACs" }
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def sync_floorplan_sizes(network_id : String? = nil)
+ network_id = network_id.presence || @default_network
+ logger.debug { "syncing floor plan sizes for network #{network_id}" }
+
+ floor_plans = {} of String => FloorPlan
+
+ req("/api/v1/networks/#{network_id}/floorPlans") { |response|
+ Array(FloorPlan).from_json(response).each do |plan|
+ floor_plans[plan.id] = plan
+ end
+ nil
+ }
+
+ @floorplan_sizes = floor_plans
+
+ # mac address => device location
+ network_devices = {} of String => NetworkDevice
+
+ req("/api/v1/networks/#{network_id}/devices") { |response|
+ Array(NetworkDevice).from_json(response).each do |device|
+ next unless device.floor_plan_id
+ network_devices[format_mac(device.mac)] = device
+ end
+ nil
+ }
+
+ @network_devices = network_devices
+
+ {floor_plans, network_devices}
+ end
+
+ # Webhook endpoint for scanning API, expects version 3
+ def parse_new_locations(payload : String) : Nil
+ logger.debug { payload } if @debug_payload
+
+ locations_updated = 0
+
+ # Parse the data posted
+ begin
+ observations = Array(Observation).from_json(payload)
+ logger.debug { "parsed meraki payload" }
+
+ ignore_older = @max_location_age.ago.in Time::Location::UTC
+ drift_older = @drift_location_age.ago.in Time::Location::UTC
+ current_time = Time.utc
+ current_time_unix = current_time.to_unix
+
+ observations.each do |observation|
+ client_mac = format_mac(observation.client_mac)
+ existing = @locations[client_mac]?
+
+ logger.debug { "parsing new observation for #{client_mac}" } if @debug_webhook
+ location = parse(existing, ignore_older, drift_older, observation)
+ if location
+ @locations[client_mac] = location
+ locations_updated += 1
+ end
+ update_ipv4(observation.ipv4, client_mac, current_time)
+ update_ipv6(observation.ipv6.try(&.downcase), client_mac, current_time)
+ end
+ rescue e
+ logger.error { "failed to parse meraki scanning API payload\n#{e.inspect_with_backtrace}" }
+ logger.debug { "failed payload body was\n#{payload}" }
+ end
+
+ logger.debug { "updated #{locations_updated} locations" }
+ end
+
+ protected def parse(existing, ignore_older, drift_older, observation) : Location?
+ locations_raw = observation.locations
+
+ # We'll attempt to return a location based on the nearest WAP
+ if locations_raw.empty?
+ last_seen = observation.latest_record
+ if wap_device = @network_devices[format_mac(last_seen.nearest_ap_mac)]?
+ return wap_device.location unless wap_device.location.nil?
+
+ if floor_plan = @floorplan_sizes[wap_device.floor_plan_id.not_nil!]?
+ return wap_device.location = Location.calculate_location(floor_plan, wap_device, last_seen.time)
+ end
+ end
+ return nil
+ end
+
+ # existing.time is our ajusted time
+ if existing_time = existing.try &.time
+ existing = nil if existing_time < ignore_older
+ end
+
+ # remove locations that don't have an x,y or very uncertain or very old
+ locations = locations_raw.reject do |loc|
+ loc.get_x.nil? || loc.variance > @maximum_uncertainty
+ end
+
+ if locations.empty?
+ logger.debug {
+ if locations_raw.empty?
+ "ignored as no location data provided"
+ else
+ "ignored as no location in observation met minimum requirements, had coordinates: #{!!locations_raw[0].get_x}, uncertainty: #{locations_raw[0].variance}"
+ end
+ } if @debug_webhook
+ return existing
+ end
+
+ # ensure oldest -> newest (we adjusted these already)
+ locations = locations.sort { |a, b| a.time <=> b.time }
+
+ # estimate the location given the current observations
+ location = existing || locations.shift
+ locations.each do |new_loc|
+ next unless new_loc.time >= location.time
+
+ # If acceptable then this is newer
+ if new_loc.variance < @acceptable_confidence
+ location = new_loc
+ next
+ end
+
+ # if more accurate and newer then we'll take this
+ if new_loc.variance < location.variance
+ location = new_loc
+ next
+ end
+
+ # should we drift the older location towards a less accurate newer location
+ if location.time < drift_older
+ # has the floor changed, we should probably accept the newer less accurate location
+ if location.floor_plan_id != new_loc.floor_plan_id
+ location = new_loc
+ next
+ end
+
+ new_uncertainty = new_loc.variance
+ old_uncertainty = location.variance
+
+ confidence_factor = 1.0 - (@confidence_multiplier * (new_uncertainty - @acceptable_confidence))
+ confidence_factor = 0.0 if confidence_factor < 0
+
+ time_diff = new_loc.time.to_unix - location.time.to_unix
+ time_factor = @time_multiplier * (time_diff - @confidence_time.to_i).to_f
+ time_factor = 0.0 if time_factor < 0
+
+ # Average of the confidence factors
+ average_multiplier = (confidence_factor + time_factor) / 2.0
+
+ new_x = new_loc.x!
+ new_y = new_loc.y!
+ old_x = location.x!
+ old_y = location.y!
+
+ # 7.5 = 5 + (( 10 - 5 ) * 0.5)
+ new_x = old_x + ((new_x - old_x) * average_multiplier)
+ new_y = old_y + ((new_y - old_y) * average_multiplier)
+ new_uncertainty = old_uncertainty + ((new_uncertainty - old_uncertainty) * average_multiplier)
+
+ new_loc.x = new_x
+ new_loc.y = new_y
+ new_loc.variance = new_uncertainty
+ location = new_loc
+ end
+ end
+
+ location
+ end
+
+ protected def update_ipv4(ipv4, client_mac, current_time)
+ return unless ipv4
+
+ lookup = @ip_lookup[ipv4]? || Lookup.new(current_time, client_mac)
+ lookup.time = current_time
+ lookup.mac = client_mac
+ @ip_lookup[ipv4] = lookup
+
+ if lookup = @ip_usernames[ipv4]?
+ username = lookup.mac
+ user_mac_mappings { |storage| map_user_mac(client_mac, username, storage) }
+ end
+ end
+
+ protected def update_ipv6(ipv6, client_mac, current_time)
+ return unless ipv6
+
+ lookup = @ip_lookup[ipv6]? || Lookup.new(current_time, client_mac)
+ lookup.time = current_time
+ lookup.mac = client_mac
+ @ip_lookup[ipv6] = lookup
+
+ if lookup = @ip_usernames[ipv6]?
+ username = lookup.mac
+ user_mac_mappings { |storage| map_user_mac(client_mac, username, storage) }
+ end
+ end
+
+ def format_mac(address : String)
+ address.gsub(/(0x|[^0-9A-Fa-f])*/, "").downcase
+ end
+
+ # ip => {username, time}
+ @ip_usernames : Hash(String, Lookup) = {} of String => Lookup
+
+ @[Security(PlaceOS::Driver::Level::Administrator)]
+ def ip_username_mappings(ip_map : Array(Tuple(String, String, String, String?))) : Nil
+ now = Time.utc
+ user_mac_mappings do |storage|
+ ip_map.each do |(ip, username, domain, hostname)|
+ username = format_username(username)
+ @ip_usernames[ip] = Lookup.new(now, username)
+
+ if lookup = @ip_lookup[ip]?
+ map_user_mac(lookup.mac, username, storage)
+ end
+ end
+ end
+ end
+
+ @[Security(PlaceOS::Driver::Level::Administrator)]
+ def mac_address_mappings(username : String, macs : Array(String), domain : String = "")
+ username = format_username(username)
+ user_mac_mappings do |storage|
+ macs.each { |mac| map_user_mac(format_mac(mac), username, storage) }
+ end
+ end
+end
diff --git a/drivers/cisco/meraki/meraki_locations_spec.cr b/drivers/cisco/meraki/meraki_locations_spec.cr
new file mode 100644
index 00000000000..ee1143e7086
--- /dev/null
+++ b/drivers/cisco/meraki/meraki_locations_spec.cr
@@ -0,0 +1,86 @@
+require "./scanning_api"
+
+class Dashboard < DriverSpecs::MockDriver
+ def fetch(location : String)
+ logger.info { "fetching: #{location}" }
+ case location
+ when "/api/v1/networks/network_id/floorPlans"
+ %([{"floorPlanId":"floor-123","name":"Level 1","width":30.5,"height":20,"topLeftCorner":{"lat":0,"lng":0},"bottomLeftCorner":{"lat":0,"lng":0},"bottomRightCorner":{"lat":0,"lng":0}}])
+ when "/api/v1/networks/network_id/devices"
+ %([])
+ end
+ end
+end
+
+DriverSpecs.mock_driver "Cisco::Meraki::Locations" do
+ system({
+ Dashboard: {Dashboard},
+ })
+
+ sleep 0.5
+
+ # Should standardise the format of MAC addresses
+ exec(:format_mac, "0x12:34:A6-789B").get.should eq %(1234a6789b)
+
+ floors_raw = %({"g_727894289773756676": {
+ "floorPlanId": "g_727894289773756676",
+ "width": 84.73653902424,
+ "height": 55.321510873304,
+ "topLeftCorner": {
+ "lat": 25.20105494120424,
+ "lng": 55.27527794417147
+ },
+ "bottomLeftCorner": {
+ "lat": 25.20128402691947,
+ "lng": 55.27478983574903
+ },
+ "bottomRightCorner": {
+ "lat": 25.200607564298647,
+ "lng": 55.27440203743774
+ },
+ "name": "BUILDING - L3"
+ },
+ "g_727894289773756679": {
+ "floorPlanId": "g_727894289773756679",
+ "width": 82.037895885132,
+ "height": 48.035263155936,
+ "topLeftCorner": {
+ "lat": 25.201070920997147,
+ "lng": 55.27523029269689
+ },
+ "bottomLeftCorner": {
+ "lat": 25.20126383588677,
+ "lng": 55.274803104166594
+ },
+ "bottomRightCorner": {
+ "lat": 25.200603702563107,
+ "lng": 55.27443896882145
+ },
+ "name": "Building - GF"
+ }})
+ floors = Hash(String, Cisco::Meraki::FloorPlan).from_json(floors_raw)
+
+ macs_raw = %({"683a1e545b0c": {
+ "floorPlanId": "g_727894289773756676",
+ "lat": 25.2011012305148,
+ "lng": 55.2749184519053,
+ "mac": "68:3a:1e:54:5b:0c",
+ "name": "1F-07"
+ },
+ "683a1e5474ed": {
+ "floorPlanId": "g_727894289773756679",
+ "lat": 25.2008175846893,
+ "lng": 55.2746475487948,
+ "mac": "68:3a:1e:54:74:ed",
+ "name": "GF-29"
+ }})
+ macs = Hash(String, Cisco::Meraki::NetworkDevice).from_json(macs_raw)
+
+ macs.each do |_mac, wap_device|
+ floor_plan = floors[wap_device.floor_plan_id]
+ # do some unit testing
+ loc = Cisco::Meraki::Location.calculate_location(floor_plan, wap_device, Time.utc)
+ pp! loc
+ loc.to_json
+ end
+end
diff --git a/drivers/cisco/meraki/scanning_api.cr b/drivers/cisco/meraki/scanning_api.cr
new file mode 100644
index 00000000000..577f044f4ab
--- /dev/null
+++ b/drivers/cisco/meraki/scanning_api.cr
@@ -0,0 +1,219 @@
+module Cisco; end
+
+require "json"
+require "./geo"
+
+module Cisco::Meraki
+ ISO8601 = "%FT%T%z"
+
+ class FloorPlan
+ include JSON::Serializable
+
+ @[JSON::Field(key: "floorPlanId")]
+ property id : String
+ property width : Float64
+ property height : Float64
+
+ @[JSON::Field(key: "topLeftCorner")]
+ property top_left : Geo::Point
+
+ @[JSON::Field(key: "bottomLeftCorner")]
+ property bottom_left : Geo::Point
+
+ @[JSON::Field(key: "bottomRightCorner")]
+ property bottom_right : Geo::Point
+
+ # This is useful for when we have to map meraki IDs to our zones
+ property name : String?
+
+ def to_distance
+ Geo::Distance.new(width, height)
+ end
+ end
+
+ class NetworkDevice
+ include JSON::Serializable
+
+ # Used for caching the location calculated for this device
+ # where an observation doesn't have location values but has a closest WAP
+ @[JSON::Field(ignore: true)]
+ property location : Location?
+
+ @[JSON::Field(key: "floorPlanId")]
+ property floor_plan_id : String?
+
+ property lat : Float64
+ property lng : Float64
+ property mac : String
+
+ # This is useful for when we have to map meraki IDs to our zones
+ property name : String?
+ end
+
+ class Client
+ include JSON::Serializable
+
+ property id : String
+ property mac : String
+ property description : String?
+
+ property ip : String?
+ property ip6 : String?
+
+ @[JSON::Field(key: "ip6Local")]
+ property ip6_local : String?
+
+ property user : String?
+
+ # 2020-09-29T07:53:08Z
+ @[JSON::Field(key: "firstSeen")]
+ property first_seen : String
+
+ @[JSON::Field(key: "lastSeen")]
+ property last_seen : String
+
+ property manufacturer : String?
+ property os : String?
+
+ @[JSON::Field(key: "recentDeviceMac")]
+ property recent_device_mac : String?
+ property ssid : String?
+ property vlan : Int32?
+ property switchport : String?
+ property status : String
+ property notes : String?
+
+ @[JSON::Field(ignore: true)]
+ property! time_added : Time
+ end
+
+ class RSSI
+ include JSON::Serializable
+
+ @[JSON::Field(key: "apMac")]
+ property access_point_mac : String
+ property rssi : Int32
+ end
+
+ class Location
+ include JSON::Serializable
+
+ def initialize(@x, @y, @lng, @lat, @variance, @floor_plan_id, @floor_plan_name, @time)
+ @mac = nil
+ @client = nil
+ @rssi_records = [] of RSSI
+ @nearest_ap_tags = [] of String
+ end
+
+ def self.calculate_location(floor : FloorPlan, device : NetworkDevice, time : Time) : Location
+ distance = Geo.calculate_xy(floor.top_left, floor.bottom_left, floor.bottom_right, device, floor.to_distance)
+ Location.new(distance.x, distance.y, device.lng, device.lat, 25_f64, floor.id, floor.name, time)
+ end
+
+ # NOTE:: This is not part of the location response,
+ # it is here to simplify processing
+ @[JSON::Field(ignore: true)]
+ property mac : String?
+
+ # NOTE:: this is not part of the location response,
+ # it is here to speed up processing
+ @[JSON::Field(ignore: true)]
+ property client : Client? = nil
+
+ # Multiple types as the location when parsed might include javascript `"NaN"`
+ property x : Float64 | String | Nil
+ property y : Float64 | String | Nil
+ property lng : Float64?
+ property lat : Float64?
+ property variance : Float64
+
+ @[JSON::Field(key: "floorPlanId")]
+ property floor_plan_id : String?
+
+ @[JSON::Field(key: "floorPlanName")]
+ property floor_plan_name : String?
+
+ @[JSON::Field(converter: Time::Format.new(Cisco::Meraki::ISO8601))]
+ property time : Time
+
+ @[JSON::Field(key: "nearestApTags")]
+ property nearest_ap_tags : Array(String)
+
+ @[JSON::Field(key: "rssiRecords")]
+ property rssi_records : Array(RSSI)
+
+ def x!
+ get_x.not_nil!
+ end
+
+ def y!
+ get_y.not_nil!
+ end
+
+ def get_x : Float64?
+ if tmp = x
+ if tmp.is_a?(Float64)
+ tmp
+ end
+ end
+ end
+
+ def get_y : Float64?
+ if tmp = y
+ if tmp.is_a?(Float64)
+ tmp
+ end
+ end
+ end
+ end
+
+ class LatestRecord
+ include JSON::Serializable
+
+ @[JSON::Field(key: "nearestApMac")]
+ property nearest_ap_mac : String
+
+ @[JSON::Field(key: "nearestApRssi")]
+ property nearest_ap_rssi : Int32
+
+ @[JSON::Field(converter: Time::Format.new(Cisco::Meraki::ISO8601))]
+ property time : Time
+ end
+
+ class Observation
+ include JSON::Serializable
+
+ @[JSON::Field(key: "clientMac")]
+ property client_mac : String
+
+ property manufacturer : String?
+ property ipv4 : String?
+ property ipv6 : String?
+ property ssid : String?
+ property os : String?
+
+ @[JSON::Field(key: "latestRecord")]
+ property latest_record : LatestRecord
+ property locations : Array(Location)
+ end
+
+ class Data
+ include JSON::Serializable
+
+ @[JSON::Field(key: "networkId")]
+ property network_id : String
+ property observations : Array(Observation)
+ end
+
+ class DevicesSeen
+ include JSON::Serializable
+
+ property version : String
+ property secret : String
+
+ @[JSON::Field(key: "type")]
+ property message_type : String
+
+ property data : Data
+ end
+end
diff --git a/drivers/cisco/switch/snooping_catalyst.cr b/drivers/cisco/switch/snooping_catalyst.cr
new file mode 100644
index 00000000000..c9b38577cff
--- /dev/null
+++ b/drivers/cisco/switch/snooping_catalyst.cr
@@ -0,0 +1,267 @@
+module Cisco; end
+
+module Cisco::Switch; end
+
+require "set"
+
+class Cisco::Switch::SnoopingCatalyst < PlaceOS::Driver
+ # Discovery Information
+ descriptive_name "Cisco Catalyst Switch IP Snooping"
+ generic_name :Snooping
+ tcp_port 22
+
+ # Communication settings
+ # tokenize delimiter: /\n|-- /
+
+ default_settings({
+ ssh: {
+ username: :cisco,
+ password: :cisco,
+ },
+ building: "building_code",
+ ignore_macs: {
+ "Cisco Phone Dock" => "7001b5",
+ },
+ })
+
+ # Interfaces that indicate they have a device connected
+ @check_interface = ::Set(String).new
+
+ # MAC, IP, Interface
+ @snooping = [] of Tuple(String, String, String)
+
+ # interface to MAC address mappings
+ @interface_macs = {} of String => String
+ @devices = {} of String => NamedTuple(mac: String, ip: String)
+
+ @hostname : String? = nil
+ @switch_name : String? = nil
+ @ignore_macs = ::Set(String).new
+
+ def on_load
+ # "--More--" is sent without a newline
+ transport.tokenizer = Tokenizer.new("\n", "--More--")
+
+ on_update
+ end
+
+ def on_update
+ @ignore_macs = ::Set.new((setting?(Hash(String, String), :ignore_macs) || {} of String => String).values)
+
+ self[:name] = @switch_name = setting?(String, :switch_name)
+ self[:ip_address] = config.ip.not_nil!.downcase
+ self[:building] = setting?(String, :building)
+ self[:level] = setting?(String, :level)
+ self[:last_successful_query] ||= 0
+ end
+
+ def connected
+ schedule.in(1.second) { query_connected_devices }
+ schedule.every(1.minute) { query_connected_devices }
+ end
+
+ def disconnected
+ schedule.clear
+ queue.clear
+ end
+
+ # Don't want the every day user using this method
+ @[Security(Level::Administrator)]
+ def run(command : String)
+ do_send command
+ end
+
+ def query_interface_status
+ do_send "show interfaces status"
+ end
+
+ def query_mac_addresses
+ @interface_macs.clear
+ do_send "show mac address-table"
+ end
+
+ def query_snooping_bindings
+ @snooping.clear
+ do_send "show ip dhcp snooping binding"
+ end
+
+ @querying_devices : Bool = false
+
+ def query_connected_devices
+ return if @querying_devices
+ @querying_devices = true
+
+ logger.debug { "Querying for connected devices" }
+
+ query_interface_status.get
+ sleep 3.seconds
+
+ query_mac_addresses.get
+ sleep 3.seconds
+
+ query_snooping_bindings.get
+ sleep 2.seconds
+
+ nil
+ ensure
+ @querying_devices = false
+ end
+
+ def received(data, task)
+ data = String.new(data)
+ logger.debug { "Switch sent: #{data}" }
+
+ # determine the hostname
+ if @hostname.nil?
+ parts = data.split(">")
+ if parts.size == 2
+ self[:hostname] = @hostname = parts[0]
+
+ # Exit early as this line is not a response
+ return task.try &.success
+ end
+ end
+
+ case data
+ when /More/
+ # Detect more data available
+ # ==> --More--
+ send(" ", priority: 99, retries: 0)
+ return task.try &.success
+ when /STATIC|DYNAMIC/
+ # Interface MAC Address detection
+ # 33 e4b9.7aa5.aa7f STATIC Gi3/0/8
+ # 10 f4db.e618.10a4 DYNAMIC Te2/0/40
+ parts = data.split(/\s+/).reject(&.empty?)
+ mac = format(parts[1])
+ interface = normalise(parts[-1])
+
+ @interface_macs[interface] = mac if mac && interface
+
+ return :success
+ when /%LINK/
+ # Interface change detection
+ # 07-Aug-2014 17:28:26 %LINK-I-Up: gi2
+ # 07-Aug-2014 17:28:31 %STP-W-PORTSTATUS: gi2: STP status Forwarding
+ # 07-Aug-2014 17:44:43 %LINK-I-Up: gi2, aggregated (1)
+ # 07-Aug-2014 17:44:47 %STP-W-PORTSTATUS: gi2: STP status Forwarding, aggregated (1)
+ # 07-Aug-2014 17:45:24 %LINK-W-Down: gi2, aggregated (2)
+ interface = normalise(data.split(",")[0].split(/\s/)[-1])
+
+ if data =~ /Up:/
+ logger.debug { "Notify Up: #{interface}" }
+ @check_interface << interface
+
+ # Delay here is to give the PC some time to negotiate an IP address
+ # schedule.in(3000) { query_snooping_bindings }
+ elsif data =~ /Down:/
+ logger.debug { "Notify Down: #{interface}" }
+ # We are no longer interested in this interface
+ @check_interface.delete(interface)
+ end
+
+ self[:interfaces] = @check_interface
+
+ return task.try &.success
+ when .starts_with?("Total number")
+ logger.debug { "Processing #{@snooping.size} bindings" }
+ checked = Set(String).new
+ devices = {} of String => NamedTuple(mac: String, ip: String)
+ state_changed = false
+
+ @snooping.each do |mac, ip, interface|
+ next unless @check_interface.includes?(interface)
+ next unless @interface_macs[interface]? == mac
+ next if checked.includes?(interface)
+
+ checked << interface
+ iface = @devices[interface]? || {mac: "", ip: ""}
+
+ if iface[:ip] != ip || iface[:mac] != mac
+ logger.debug { "New connection on #{interface} with #{ip}: #{mac}" }
+ devices[interface] = {mac: mac, ip: ip}
+ state_changed = true
+ else
+ devices[interface] = iface
+ end
+ end
+
+ # did an interface change state
+ if state_changed
+ @devices = devices
+ self[:devices] = devices
+ end
+
+ # As a link up or down might have modified this list
+ if @check_interface != checked
+ @check_interface = checked
+ self[:interfaces] = checked
+ end
+
+ self[:last_successful_query] = Time.utc.to_unix
+
+ return task.try &.success
+ end
+
+ # Grab the parts of the response
+ entries = data.split(/\s+/).reject(&.empty?)
+
+ # show interfaces status
+ # Port Name Status Vlan Duplex Speed Type
+ # Gi1/1 notconnect 1 auto auto No Gbic
+ # Fa6/1 connected 1 a-full a-100 10/100BaseTX
+ case entries
+ when .includes?("connected")
+ interface = entries[0].downcase
+ unless @check_interface.includes? interface
+ logger.debug { "Interface Up: #{interface}" }
+ @check_interface << interface
+ end
+ when .includes?("notconnect")
+ interface = entries[0].downcase
+ if @check_interface.includes? interface
+ # Delete the lookup records
+ logger.debug { "Interface Down: #{interface}" }
+ @check_interface.delete(interface)
+ end
+ else
+ if entries.size > 2
+ # We are looking for MAC to IP address mappings
+ # =============================================
+ # MacAddress IpAddress Lease(sec) Type VLAN Interface
+ # ------------------ --------------- ---------- ------------- ---- --------------------
+ # 00:21:CC:D5:33:F4 10.151.130.1 16283 dhcp-snooping 113 GigabitEthernet3/0/43
+ # Total number of bindings: 3
+ interface = normalise(entries[-1])
+
+ # We only want entries that are currently active
+ if @check_interface.includes? interface
+ # Ensure the data is valid
+ mac = entries[0]
+ if mac =~ /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
+ mac = format(mac)
+ ip = entries[1]
+
+ @snooping << {mac, ip, interface} unless @ignore_macs.includes?(mac[0..5])
+ end
+ end
+ end
+ end
+
+ task.try &.success
+ end
+
+ protected def do_send(cmd, **options)
+ logger.debug { "requesting: #{cmd}" }
+ send("#{cmd}\n", **options)
+ end
+
+ protected def format(mac)
+ mac.gsub(/(0x|[^0-9A-Fa-f])*/, "").downcase
+ end
+
+ protected def normalise(interface)
+ # Port-channel == po
+ interface.downcase.gsub("tengigabitethernet", "te").gsub("twogigabitethernet", "tw").gsub("gigabitethernet", "gi").gsub("fastethernet", "fa")
+ end
+end
diff --git a/drivers/cisco/switch/snooping_catalyst_spec.cr b/drivers/cisco/switch/snooping_catalyst_spec.cr
new file mode 100644
index 00000000000..2cb082d8412
--- /dev/null
+++ b/drivers/cisco/switch/snooping_catalyst_spec.cr
@@ -0,0 +1,61 @@
+DriverSpecs.mock_driver "Cisco::Switch::SnoopingCatalyst" do
+ transmit "SG-MARWFA61301>"
+ sleep 1.5.seconds
+
+ should_send "show interfaces status\n"
+ transmit "show interfaces status\n"
+ status[:hostname].should eq("SG-MARWFA61301")
+
+ transmit %(Port Name Status Vlan Duplex Speed Type
+Gi1/0/1 notconnect 113 auto auto 10/100/1000BaseTX
+Gi1/0/2 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/11 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/12 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/13 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/14 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/15 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/16 notconnect 113 auto auto 10/100/1000BaseTX
+Gi2/0/17 notconnect 113 auto auto 10/100/1000BaseTX
+Gi3/0/8 connected 33 auto auto 10/100/1000BaseTX
+ --More--)
+
+ should_send " "
+ transmit %(
+Gi4/0/48 notconnect 113 auto auto 10/100/1000BaseTX
+Gi4/1/1 notconnect 1 auto auto unknown
+Gi4/1/2 notconnect 1 auto auto unknown
+Te4/1/4 connected trunk full 10G SFP-10GBase-SR
+Po1 connected trunk a-full a-10G
+)
+
+ sleep 3.1.seconds
+
+ should_send "show mac address-table\n"
+ transmit "show mac address-table\n"
+
+ transmit %(Vlan MAC Type Port
+33 e4b9.7aa5.aa7f STATIC Gi3/0/8
+10 f4db.e618.10a4 DYNAMIC Te2/0/40
+)
+
+ sleep 3.1.seconds
+
+ should_send "show ip dhcp snooping binding\n"
+ transmit %(MacAddress IpAddress Lease(sec) Type VLAN Interface
+------------------ --------------- ---------- ------------- ---- --------------------
+38:C9:86:17:A2:07 192.168.1.15 19868 dhcp-snooping 113 tenGigabitEthernet4/1/4
+E4:B9:7A:A5:AA:7F 10.151.128.150 16532 dhcp-snooping 33 GigabitEthernet3/0/8
+00:21:CC:D5:33:F4 10.151.130.1 16283 dhcp-snooping 113 GigabitEthernet3/0/34
+Total number of bindings: 3
+
+)
+
+ status["devices"].should eq({
+ "gi3/0/8" => {
+ "mac" => "e4b97aa5aa7f",
+ "ip" => "10.151.128.150",
+ },
+ })
+
+ status["interfaces"].should eq(["gi3/0/8"])
+end
diff --git a/drivers/denon/amplifier/av_receiver.cr b/drivers/denon/amplifier/av_receiver.cr
new file mode 100644
index 00000000000..ecc032326dc
--- /dev/null
+++ b/drivers/denon/amplifier/av_receiver.cr
@@ -0,0 +1,214 @@
+require "digest/md5"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/switchable"
+
+#
+module Denon; end
+
+module Denon::Amplifier; end
+
+# Protocol: https://aca.im/driver_docs/Denon/Denon%20AVR%20PROTOCOL%20V7.5.0.pdf
+#
+# NOTE:: Denon doesn't respond to commands that request the current state
+# (ie if the volume is 100 and you request 100 it will not respond)
+#
+
+class Denon::Amplifier::AvReceiver < PlaceOS::Driver
+ include PlaceOS::Driver::Interface::Powerable
+ include PlaceOS::Driver::Utilities::Transcoder
+
+ @channel : Channel(String) = Channel(String).new
+ @stable_power : Bool = true
+
+ COMMANDS = {
+ power: :PW,
+ power_query: :PW?,
+ mute: :MU,
+ mute_query: :MU?,
+ volume: :MV,
+ volume_query: :MV?,
+ input: :SI,
+ input_query: :SI?,
+ }
+ COMMANDS.to_h.merge!(COMMANDS.to_h.invert)
+
+ @volume_range = 0..196
+
+ default_settings({
+ max_waits: 10,
+ timeout: 3000,
+ })
+ # Discovery Information
+ tcp_port 23 # Telnet
+ descriptive_name "Denon AVR (Switcher Amplifier)"
+ generic_name :Switcher
+
+ # Denon requires some breathing room
+ # delay between_sends: 30
+ # delay on_receive: 30
+
+ def on_load
+ # transport.tokenizer = Tokenizer.new(Bytes[0x0D])
+ transport.tokenizer = Tokenizer.new("\r")
+ self[:volume_min] = 0
+ self[:volume_max] = @volume_range.max # == 98 * 2 - Times by 2 so we can account for the half steps
+ on_update
+ end
+
+ def on_update
+ self[:max_waits] = 10
+ self[:timeout] = 3000
+ end
+
+ def connected
+ #
+ # Get state
+ #
+ # power?
+ # input?
+ # mute?
+
+ schedule.every(60.seconds) do
+ logger.info { "-- Polling Denon AVR" }
+ power?
+ do_send(:input, priority: 0, name: :input)
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool = false)
+ # self[:power] is current as we would be informed otherwise
+ if state && (self[:power] == "OFF" || self[:power] == "STANDBY") # Request to power on if off
+ do_send(:power, "ON", delay: 3.milliseconds, name: :power) # Manual states delay for 1 second, just to be safe
+ elsif !state && self[:power] == "ON" # Request to power off if on
+ do_send(:power, "STANDBY", delay: 3.milliseconds, name: :power)
+ end
+ end
+
+ def power?
+ # def power?(**options)
+ # options[:emit] = {:power => block} unless block.nil?
+ do_send(:power_query, priority: 0, name: :power_query)
+ end
+
+ def mute?
+ self[:mute] = "OFF"
+ do_send(:mute_query, priority: 0, name: :mute_query)
+ end
+
+ def mute(state : Bool = true)
+ req = state ? "ON" : "OFF"
+ return if self[:mute] == req
+ do_send(:mute, req, name: :mute)
+ end
+
+ def mute_audio(state : Bool = true)
+ mute state
+ end
+
+ def unmute
+ mute false
+ end
+
+ def unmute_audio
+ unmute
+ end
+
+ def volume(level : Int32 = 0)
+ value = 0
+ value = level if @volume_range.includes?(level.to_i)
+
+ return if self[:volume] == value
+
+ # The denon is weird 99 is volume off,
+ # 99.5 is the minimum volume,
+ # 0 is the next lowest volume and 985 is the loudest volume
+ # => So we are treating 99, 995 and 0 as 0
+ step = value % 2
+ actual = value / 2
+ req = actual.to_s.rjust(2, '0')
+ req += "5" if step != 0
+
+ do_send(:volume, req, name: :volume) # Name prevents needless queuing of commands
+
+ end
+
+ def volume?
+ do_send(:volume_query, priority: 0, name: :volume_query)
+ end
+
+ # Just here for documentation (there are many more)
+ #
+ # INPUTS = [:cd, :tuner, :dvd, :bd, :tv, :"sat/cbl", :dvr, :game, :game2, :"v.aux", :dock]
+ def input(input : String = "")
+ status = input.upcase # .downcase.to_sym
+ if status != self[:input]
+ input = input.to_s.upcase
+ do_send(:input, input, name: :input)
+ end
+ end
+
+ def input?
+ do_send(:input_query, priority: 0, name: :input_query)
+ end
+
+ def received(data, task)
+ data = String.new(data)
+ logger.info { "Denon sent #{data.inspect}" }
+
+ return unless task
+
+ # Process the response
+ cmd = data[0..1] # first 2 chars are the key / command
+ val = data[2..-2] # anything following the above and before \r is a response value
+
+ case cmd
+ when "PW"
+ self[:power] = val
+ when "SI"
+ self[:input] = val
+ when "MV"
+ # return :ignore if val.chars.size > 3 # May send 'MVMAX 98' after volume command
+ # self[:volume] = 0
+ # vol = val.to_i32
+ # self[:volume] = val unless val.to_i32 > @volume_range.max
+ self[:volume] = val
+ # return :ignore if param.length > 3 # May send 'MVMAX 98' after volume command
+ # vol = param[0..1].to_i * 2
+ # vol += 1 if param.length == 3
+ # vol == 0 if vol > @volume_range.max # this means the volume was 99 or 995
+ # self[:volume] = vol
+
+ when "MU"
+ self[:mute] = val
+ else
+ return :ignore
+ end
+
+ task.try &.success
+ end
+
+ protected def do_send(command, param = nil, **options)
+ # prepare the command
+ cmd = if param.nil?
+ "#{COMMANDS[command]}"
+ else
+ "#{COMMANDS[command]}#{param}"
+ end
+ logger.info { "Queing: #{cmd}" }
+
+ # queue the request
+ queue(**({
+ name: command,
+ }.merge(options))) do
+ @channel = Channel(String).new
+ # send the request
+ logger.info { " Sending: #{cmd}" }
+ transport.send(cmd)
+ end
+ end
+end
diff --git a/drivers/denon/amplifier/av_receiver_spec.cr b/drivers/denon/amplifier/av_receiver_spec.cr
new file mode 100644
index 00000000000..f844a6d7385
--- /dev/null
+++ b/drivers/denon/amplifier/av_receiver_spec.cr
@@ -0,0 +1,71 @@
+DriverSpecs.mock_driver "Denon::Amplifier::AvReceiver" do
+ ####
+ # POWER
+ #
+ sleep 1.second
+ # query power
+ exec(:power?)
+ should_send("PW?")
+ responds("PWOFF\r")
+ status[:power].should eq("OFF")
+ # turn power on
+ exec(:power, true)
+ should_send("PWON")
+ responds("PWON\r")
+ status[:power].should eq("ON")
+ # power off turns amp to STANDBY not actually OFF
+ exec(:power, false)
+ should_send("PWSTANDBY")
+ responds("PWSTANDBY\r")
+ status[:power].should eq("STANDBY")
+
+ ####
+ # INPUT
+ #
+ sleep 1.second
+ # query input > DVD
+ exec(:input?)
+ should_send("SI?")
+ responds("SIDVD\r")
+ status[:input].should eq("DVD")
+ # chaange input to tuner
+ exec(:input, "TUNER")
+ should_send("SITUNER")
+ responds("SITUNER\r")
+ status[:input].should eq("TUNER")
+
+ ####
+ # VOLUME
+ #
+ sleep 1.second
+ # query
+ exec(:volume?)
+ should_send("MV?")
+ responds("MV80\r")
+ status[:volume].should eq("80")
+ # change volume
+ exec(:volume, 78)
+ should_send("MV39.0")
+ responds("MV39.0\r")
+ status[:volume].should eq("39.0")
+
+ ####
+ # MUTE
+ #
+ sleep 1.second
+ # query
+ exec(:mute?)
+ should_send("MU?")
+ responds("MUOFF\r")
+ status[:mute].should eq("OFF")
+ # mute on
+ exec(:mute, true)
+ should_send("MUON")
+ responds("MUON\r")
+ status[:mute].should eq("ON")
+ # mute off
+ exec(:mute, false)
+ should_send("MUOFF")
+ responds("MUOFF\r")
+ status[:mute].should eq("OFF")
+end
diff --git a/drivers/echo360/device_capture.cr b/drivers/echo360/device_capture.cr
new file mode 100644
index 00000000000..8bac83f5cc2
--- /dev/null
+++ b/drivers/echo360/device_capture.cr
@@ -0,0 +1,170 @@
+require "oq"
+
+# Documentation: https://aca.im/driver_docs/Echo360/EchoSystemCaptureAPI_v301.pdf
+
+class Echo360::DeviceCapture < PlaceOS::Driver
+ # Discovery Information
+ generic_name :Capture
+ descriptive_name "Echo365 Device Capture"
+ uri_base "https://echo.server"
+
+ default_settings({
+ basic_auth: {
+ username: "srvc_acct",
+ password: "password!",
+ },
+ })
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ schedule.clear
+ schedule.every(15.seconds) do
+ logger.debug { "-- Polling Capture" }
+ system_status
+ capture_status
+ end
+ end
+
+ STATUS_CMDS = {
+ system_status: :system,
+ capture_status: :captures,
+ next: :next_capture,
+ current: :current_capture,
+ state: :monitoring,
+ }
+
+ {% begin %}
+ {% for function, route in STATUS_CMDS %}
+ {% path = "/status/#{route.id}" %}
+ def {{function.id}}
+ response = get({{path}})
+ process_status check(response)
+ end
+ {% end %}
+ {% end %}
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def restart_application
+ post("/diagnostics/restart_all").success?
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def reboot
+ post("/diagnostics/reboot").success?
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def captures
+ response = get("/diagnostics/recovery/saved-content")
+ self[:captures] = check(response)["captures"]["capture"]
+ end
+
+ @[Security(PlaceOS::Driver::Level::Support)]
+ def upload(id : String)
+ response = post("/diagnostics/recovery/#{id}/upload")
+ raise "upload request failed with #{response.status_code}\n#{response.body}" unless response.success?
+ response.body
+ end
+
+ # This will auto-start a recording
+ def capture(name : String, duration : Int32, profile : String? = nil)
+ profile ||= self[:capture_profiles][0].as_s
+ response = post("/capture/new_capture", body: URI::Params.build { |form|
+ form.add("description", name)
+ form.add("duration", duration.to_s)
+ form.add("capture_profile_name", profile)
+ })
+ check(response)["ok"]["#text"].as_s
+ end
+
+ def test_capture(name : String, duration : Int32, profile : String? = nil)
+ profile ||= self[:capture_profiles][0].as_s
+ response = post("/capture/confidence_monitor", body: URI::Params.build { |form|
+ form.add("description", name)
+ form.add("duration", duration.to_s)
+ form.add("capture_profile_name", profile)
+ })
+ check(response)["ok"]["#text"].as_s
+ end
+
+ def extend(duration : Int32)
+ response = post("/capture/confidence_monitor", body: URI::Params.build { |form|
+ form.add("duration", duration.to_s)
+ })
+ check(response)["ok"]["#text"].as_s
+ end
+
+ def pause
+ response = post("/capture/pause")
+ check(response)["ok"]["#text"].as_s
+ end
+
+ def start
+ response = post("/capture/record")
+ check(response)["ok"]["#text"].as_s
+ end
+
+ def resume
+ start
+ end
+
+ def record
+ start
+ end
+
+ def stop
+ response = post("/capture/stop")
+ check(response)["ok"]["#text"].as_s
+ end
+
+ # Converts the response into the appropriate format and indicates success / failure
+ protected def check(response)
+ raise "request failed with #{response.status_code}\n#{response.body}" unless response.success?
+
+ # Convert the XML to JSON for simple parsing
+ # https://www.xml.com/pub/a/2006/05/31/converting-between-xml-and-json.html
+ input_io = IO::Memory.new response.body
+ output_io = IO::Memory.new
+ OQ::Converters::XML.deserialize input_io, output_io
+
+ output_io.rewind
+ json = JSON.parse(output_io)
+ logger.debug { "response was\n#{json.pretty_inspect}" }
+ json
+ end
+
+ CHECK = {"next", "current"}
+
+ # generic function for processing status and exposing the state
+ protected def process_status(data)
+ if results = data["status"]?.try(&.as_h)
+ results.each do |key, value|
+ if key.in?(CHECK) && (value.as_s?.try(&.strip.empty?) || value["schedule"]?.try(&.as_s?.try(&.strip.empty?)))
+ # next / current recordings are not present
+ self[key] = nil
+ elsif key[-1] == 's' && (hash = value.as_h?)
+ # This handles `{"api-versions" => {"api-version" => "3.0"}}`
+ inner = hash[key[0..-2]]?
+ if inner
+ self[key] = inner
+ else
+ self[key] = hash
+ end
+ elsif str_val = value.as_s?.try(&.strip)
+ # cleanup whitespace around string values
+ self[key] = str_val
+ else
+ # otherwise we don't manipulate the value and expose it for use
+ self[key] = value
+ end
+ end
+ results
+ else
+ logger.debug { "namespace 'status' not found, ignoring payload" }
+ data
+ end
+ end
+end
diff --git a/drivers/echo360/device_capture_spec.cr b/drivers/echo360/device_capture_spec.cr
new file mode 100644
index 00000000000..c4c81fa8bee
--- /dev/null
+++ b/drivers/echo360/device_capture_spec.cr
@@ -0,0 +1,180 @@
+DriverSpecs.mock_driver "Echo360::DeviceCapture" do
+ retval = exec(:system_status)
+ expect_http_request do |request, response|
+ if request.path == "/status/system"
+ response.status_code = 200
+ response << SYSTEM_STATUS
+ else
+ puts "unexpected path #{request.path}"
+ response.status_code = 404
+ end
+ end
+ retval.get
+
+ status["api-versions"].should eq "3.0"
+
+ retval = exec(:captures)
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response << CAPTURE_STATUS
+ end
+ retval.get
+ status[:captures].as_a.size.should eq(2)
+end
+
+CAPTURE_STATUS = <<-HEREDOC
+
+
+ Underwater Basket Weaving 101 (UWBW-101-100) Spring 2014
+ 2014-02-12T15:30:00.000Z
+ 3000
+ Underwater Basket Weaving 101 (UWBW-101-100) Spring 2014
+
+
+ John Doe
+
+
+
+
+ Some other capture
+ 2014-02-13T15:30:00.000Z
+ 1500
+
+
+
+ Steve
+
+
+
+
+ HEREDOC
+
+SYSTEM_STATUS = <<-HEREDOC
+
+ 2014-02-12T15:02:19.037Z
+
+ 3.0
+
+
+ Audio Only (Podcast). Balanced between file size & quality
+ Display Only (Podcast/Vodcast/EchoPlayer). Balanced between file size & quality
+ Display/Video (Podcast/Vodcast/EchoPlayer). Balanced between file size & quality
+ Display/Video (Podcast/Vodcast/EchoPlayer). Optimized for quality/full motion video
+ DualDisplay (Podcast/Vodcast/EchoPlayer). Optimized for file size & bandwidth
+ Dual Video (Podcast/Vodcast/EchoPlayer) -Balance between file size & quality
+ Dual Video (Podcast/Vodcast/EchoPlayer) -High Quality
+ Video Only (Podcast/Vodcast/EchoPlayer). Balanced between file size & quality
+
+
+ Display/Video (Podcast/Vodcast/EchoPlayer). Balanced between file size & quality
+
+
+ media
+ 2014-02-12T23:00:00.000Z
+ 3000
+
+ Underwater Basket Weaving 101 (UWBW-101-100) Spring 2014
+ Underwater Basket Weaving 101 (UWBW-101-100) Spring 2014
+
+ John Doe
+
+
+ Display/Video (Podcast/Vodcast/EchoPlayer). Optimized for quality/full motion video
+ archive
+
+
+
+ balanced
+ stereo
+ -6
+ 44100
+ 0
+ false
+
+
+ 1
+ dvi
+ 50
+ 50
+ 50
+ 10.0
+ 960
+ 720
+ true
+ true
+
+
+ 2
+ composite
+ 50
+ 50
+ 50
+ 29.97
+ 704
+ 480
+ true
+ false
+ ntsc
+
+
+ audio
+ aac
+ true
+
+ 128000
+ lc
+
+
+
+ graphics1
+ h264
+
+ vbr
+ 736000
+ 1104000
+ base
+ 50
+
+
+
+ graphics2
+ h264
+
+ vbr
+ 1056000
+ 1584000
+ base
+ 150
+
+
+
+ audio-archive
+
+ file
+ audio.aac
+
+
+
+ graphics1-archive
+
+ file
+ display.h264
+
+
+
+ graphics2-archive
+
+ file
+ video.h264
+
+
+
+
+
+
+
+
+
+
+
+ HEREDOC
diff --git a/drivers/epson/projector/esc_vp21.cr b/drivers/epson/projector/esc_vp21.cr
new file mode 100644
index 00000000000..0555e875f0b
--- /dev/null
+++ b/drivers/epson/projector/esc_vp21.cr
@@ -0,0 +1,224 @@
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/switchable"
+
+class Epson::Projector::EscVp21 < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Input
+ HDMI = 0x30
+ HDBaseT = 0x80
+ end
+
+ include Interface::InputSelection(Input)
+
+ # Discovery Information
+ tcp_port 3629
+ descriptive_name "Epson Projector"
+ generic_name :Display
+
+ @power_target : Bool? = nil
+ @unmute_volume : Int32? = nil
+
+ def on_load
+ transport.tokenizer = Tokenizer.new("\r")
+ self[:type] = :projector
+ end
+
+ def connected
+ # Have to init comms
+ send("ESC/VP.net\x10\x03\x00\x00\x00\x00")
+ schedule.every(52.seconds, true) { do_poll }
+ end
+
+ def disconnected
+ schedule.clear
+ self[:power] = false
+ end
+
+ def power(state : Bool)
+ if state
+ @power_target = true
+ logger.debug { "-- epson Proj, requested to power on" }
+ do_send(:power, "ON", delay: 40.seconds, name: "power")
+ else
+ @power_target = false
+ logger.debug { "-- epson Proj, requested to power off" }
+ do_send(:power, "OFF", delay: 10.seconds, name: "power")
+ end
+ power?
+ end
+
+ def power?(**options) : Bool
+ do_send(:power, **options, name: :power?).get
+ !!self[:power]?.try(&.as_bool)
+ end
+
+ def switch_to(input : Input)
+ logger.debug { "-- epson Proj, requested to switch to: #{input}" }
+ do_send(:input, input.value.to_s(16), name: :input)
+
+ # for a responsive UI
+ self[:input] = input # for a responsive UI
+ self[:video_mute] = false
+ input?
+ end
+
+ def input?
+ do_send(:input, name: :input_query, priority: 0).get
+ self[:input]
+ end
+
+ # Volume commands are sent using the inpt command
+ def volume(vol : Int32, **options)
+ vol = vol.clamp(0, 255)
+ @unmute_volume = self[:volume].as_i if (mute = vol == 0) && self[:volume]?
+ do_send(:volume, vol, **options, name: :volume)
+
+ # for a responsive UI
+ self[:volume] = vol
+ self[:audio_mute] = mute
+ volume?
+ end
+
+ def volume?
+ do_send(:volume, name: :volume?, priority: 0).get
+ self[:volume]?.try(&.as_i)
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ case layer
+ when .audio_video?
+ do_send(:av_mute, state ? "ON" : "OFF", name: :mute)
+ do_send(:av_mute, name: :mute?, priority: 0)
+ when .video?
+ do_send(:video_mute, state ? "ON" : "OFF", name: :video_mute)
+ video_mute?
+ when .audio?
+ val = state ? 0 : @unmute_volume.not_nil!
+ volume(val)
+ end
+ end
+
+ def video_mute?
+ do_send(:video_mute, name: :video_mute?, priority: 0).get
+ !!self[:video_mute]?.try(&.as_bool)
+ end
+
+ ERRORS = [
+ "00: no error",
+ "01: fan error",
+ "03: lamp failure at power on",
+ "04: high internal temperature",
+ "06: lamp error",
+ "07: lamp cover door open",
+ "08: cinema filter error",
+ "09: capacitor is disconnected",
+ "0A: auto iris error",
+ "0B: subsystem error",
+ "0C: low air flow error",
+ "0D: air flow sensor error",
+ "0E: ballast power supply error",
+ "0F: shutter error",
+ "10: peltiert cooling error",
+ "11: pump cooling error",
+ "12: static iris error",
+ "13: power supply unit error",
+ "14: exhaust shutter error",
+ "15: obstacle detection error",
+ "16: IF board discernment error",
+ ]
+
+ def inspect_error
+ do_send(:error, priority: 0)
+ end
+
+ COMMAND = {
+ power: "PWR",
+ input: "SOURCE",
+ volume: "VOL",
+ av_mute: "MUTE",
+ video_mute: "MSEL",
+ error: "ERR",
+ lamp: "LAMP",
+ }
+ RESPONSE = COMMAND.to_h.invert
+
+ def received(data, task)
+ return task.try(&.success) if data.size <= 2
+ data = String.new(data[1..-2])
+ logger.debug { "epson Proj sent: #{data}" }
+
+ data = data.split('=')
+ case RESPONSE[data[0]]
+ when :error
+ if data[1]?
+ code = data[1].to_i(16)
+ self[:last_error] = ERRORS[code]? || "#{data[1]}: unknown error code #{code}"
+ return task.try(&.success("Epson PJ error was #{self[:last_error]}"))
+ else # Lookup error!
+ return task.try(&.abort("Epson PJ sent error response for #{task.not_nil!.name || "unknown"}"))
+ end
+ when :power
+ state = data[1].to_i
+ self[:power] = state < 3
+ self[:warming] = state == 2
+ self[:cooling] = state == 3
+
+ if self[:warming].as_bool || self[:cooling].as_bool
+ schedule.in(5.seconds) { power?(priority: 0) }
+ end
+
+ if (power_target = @power_target) && self[:power] == power_target
+ @power_target = nil
+ self[:video_mute] = false unless self[:power].as_bool
+ end
+ when :av_mute
+ self[:video_mute] = self[:audio_mute] = data[1] == "ON"
+ self[:volume] = 0
+ when :video_mute
+ self[:video_mute] = data[1] == "ON"
+ when :volume
+ vol = data[1].to_i
+ self[:volume] = vol
+ mute = vol == 0
+ self[:audio_mute] = mute if mute
+ @unmute_volume ||= vol unless mute
+ when :lamp
+ self[:lamp_usage] = data[1].to_i
+ when :input
+ self[:input] = Input.from_value(data[1].to_i(16)) || "unknown"
+ end
+
+ task.try(&.success)
+ end
+
+ def do_poll
+ if power?(priority: 0)
+ if power_target = @power_target
+ if self[:power]? != power_target
+ power(power_target)
+ else
+ @power_target = nil
+ end
+ else
+ input?
+ video_mute?
+ volume?
+ end
+ end
+ do_send(:lamp, priority: 0)
+ end
+
+ private def do_send(command, param = nil, **options)
+ command = COMMAND[command]
+ cmd = param ? "#{command} #{param}\r" : "#{command}?\r"
+ logger.debug { "Epson proj sending #{command}: #{cmd}" }
+ send(cmd, **options)
+ end
+end
diff --git a/drivers/epson/projector/esc_vp21_spec.cr b/drivers/epson/projector/esc_vp21_spec.cr
new file mode 100644
index 00000000000..a37f904101b
--- /dev/null
+++ b/drivers/epson/projector/esc_vp21_spec.cr
@@ -0,0 +1,59 @@
+DriverSpecs.mock_driver "Epson::Projector::EscVp21" do
+ # connected
+ should_send("ESC/VP.net\x10\x03\x00\x00\x00\x00")
+ responds(":\r")
+ # do_poll
+ # power?
+ should_send("PWR?\r")
+ responds(":PWR=01\r")
+ status[:power].should eq(true)
+ # input?
+ should_send("SOURCE?\r")
+ responds(":SOURCE=30\r")
+ status[:input].should eq("HDMI")
+ # video_mute?
+ should_send("MSEL?\r")
+ responds(":MSEL=0\r")
+ status[:video_mute].should eq(false)
+ # volume?
+ should_send("VOL?\r")
+ responds(":VOL=10\r")
+ status[:volume].should eq(10)
+ # lamp
+ should_send("LAMP?\r")
+ responds(":LAMP=20\r")
+ status[:lamp_usage].should eq(20)
+
+ exec(:mute)
+ should_send("MUTE ON\r")
+ responds(":\r")
+ should_send("MUTE?\r")
+ responds(":MUTE=ON\r")
+ status[:video_mute].should eq(true)
+ status[:audio_mute].should eq(true)
+ status[:volume].should eq(0)
+
+ exec(:switch_to, "HDBaseT")
+ should_send("SOURCE 80\r")
+ responds(":\r")
+ should_send("SOURCE?\r")
+ responds(":SOURCE=80\r")
+ status[:input].should eq("HDBaseT")
+ status[:video_mute].should eq(false)
+
+ exec(:mute_audio, false)
+ should_send("VOL 10\r")
+ responds(":\r")
+ should_send("VOL?\r")
+ responds(":VOL=10\r")
+ status[:volume].should eq(10)
+ status[:audio_mute].should eq(false)
+
+ exec(:volume, 50)
+ should_send("VOL 50\r")
+ responds(":\r")
+ should_send("VOL?\r")
+ responds(":VOL=50\r")
+ status[:volume].should eq(50)
+ status[:audio_mute].should eq(false)
+end
diff --git a/drivers/exterity/avedia_player/r92xx.cr b/drivers/exterity/avedia_player/r92xx.cr
new file mode 100644
index 00000000000..1d1bda86d06
--- /dev/null
+++ b/drivers/exterity/avedia_player/r92xx.cr
@@ -0,0 +1,170 @@
+require "telnet"
+
+module Exterity; end
+
+module Exterity::AvediaPlayer; end
+
+class Exterity::AvediaPlayer::R92xx < PlaceOS::Driver
+ descriptive_name "Exterity Avedia Player (R92xx)"
+ generic_name :IPTV
+ tcp_port 23
+
+ default_settings({
+ max_waits: 100,
+ username: "admin",
+ password: "labrador",
+ })
+
+ @ready : Bool = false
+ @telnet : Telnet? = nil
+
+ def on_load
+ new_telnet_client
+ transport.pre_processor { |bytes| @telnet.try &.buffer(bytes) }
+ end
+
+ def connected
+ @ready = false
+ self[:ready] = false
+
+ schedule.every(60.seconds) do
+ logger.info { "-- Polling Exterity Player" }
+ tv_info
+ end
+ end
+
+ def disconnected
+ # ensures the buffer is cleared
+ new_telnet_client
+
+ schedule.clear
+ end
+
+ def channel(number : Int32 | String)
+ if number.is_a? Number
+ set :playChannelNumber, number
+ else
+ stream number
+ end
+ end
+
+ def stream(uri : String)
+ set :playChannelUri, uri
+ end
+
+ def dump
+ do_send "^dump!", name: :dump
+ end
+
+ def help
+ do_send "^help!", name: :help
+ end
+
+ def reboot
+ remote :reboot
+ end
+
+ def tv_info
+ get :tv_info
+ end
+
+ def version
+ get :SoftwareVersion
+ end
+
+ def manual(cmd : String)
+ do_send cmd
+ end
+
+ def received(data, task)
+ data = String.new(data).strip
+
+ logger.info { "Exterity sent #{data}" }
+
+ if @ready
+ # Detect if logged out of serialCommandInterface
+ if data =~ /sh: .* not found/i
+ # Launch command processor
+ do_send "/usr/bin/serialCommandInterface", wait: false, delay: 2.seconds, priority: 95
+ return :failure
+ end
+
+ # Extract response
+ data.split("!").map(&.strip("^")).each do |resp|
+ process_resp(resp, task)
+ end
+ elsif data =~ /Exterity Control Interface| Exit/i
+ logger.info { "-- got the control interface message, we're READY now" }
+ @ready = true
+ self[:ready] = true
+ version
+ elsif data =~ /login:/i
+ logger.info { "-- got the login: prompt" }
+ transport.tokenizer = Tokenizer.new("\r")
+
+ # login
+ do_send setting(String, :username), wait: false, delay: 200.milliseconds, priority: 98
+ do_send setting(String, :password), wait: false, delay: 200.milliseconds, priority: 97
+
+ # select open shell option
+ do_send "6", wait: false, delay: 2.seconds, priority: 96
+
+ # launch command processor
+ do_send "/usr/bin/serialCommandInterface", wait: false, delay: 200.milliseconds, priority: 95
+
+ # we need to disconnect if we don't see the serialCommandInterface after a certain amount of time
+ schedule.in(20.seconds) do
+ if !@ready
+ logger.error { "Exterity connection failed to be ready after 5 seconds. Check username and password." }
+ disconnect
+ end
+ end
+ elsif logger.info { "Somehow we got here #{data}" }
+ end
+
+ task.try &.success
+ end
+
+ protected def process_resp(data, task)
+ logger.info { "Resp details #{data}" }
+
+ parts = data.split ':'
+
+ case parts[0].to_s
+ when "error"
+ if task != nil
+ logger.warn { "Error when requesting: #{task.try &.name}" }
+ else
+ logger.warn { "Error response received" }
+ end
+ when "tv_info"
+ self[:tv_info] = parts[1]
+ when "SoftwareVersion"
+ self[:version] = parts[1]
+ end
+ end
+
+ protected def new_telnet_client
+ @telnet = Telnet.new do |data|
+ transport.send(data)
+ end
+ end
+
+ protected def do_send(command, **options)
+ logger.info { "requesting #{command}" }
+ send @telnet.not_nil!.prepare(command), **options
+ end
+
+ protected def set(command, data, **options)
+ # options[:name] = :"set_#{command}" unless options[:name]
+ do_send "^set:#{command}:#{data}!", **options
+ end
+
+ protected def remote(cmd, **options)
+ do_send "^send:#{cmd}!", **options
+ end
+
+ protected def get(status, **options)
+ do_send "^get:#{status}!", **options
+ end
+end
diff --git a/drivers/exterity/avedia_player/r92xx_protocol.md b/drivers/exterity/avedia_player/r92xx_protocol.md
new file mode 100644
index 00000000000..fdad269cab1
--- /dev/null
+++ b/drivers/exterity/avedia_player/r92xx_protocol.md
@@ -0,0 +1,415 @@
+
+# Exterity AvediaPlayer r9200 Control Protocol.
+
+NOTE:: All information in this document was obtained via exploration of the R9200 device.
+No information here was provided by Exterity during this process
+
+
+## Connecting
+
+* Telnet Protocol (port 23)
+* `telnet 192.168.1.13`
+* Default username: `admin`
+* Default password: `labrador`
+* Select option `6` to run a shell
+
+
+## Shell Navigation
+
+Once in the shell you can use following tools to read files:
+
+* `less` for scanning through files
+* `cat` for dumping files
+* `ps aux` for viewing processes
+* `ls` for listing files
+
+The file system is readonly so moving files to `/usr/local/www` for downloading was not possible.
+
+
+## Applications
+
+* Application are installed at: `/usr/bin`
+ * `serialCommandInterface` allows programmatic control of the device
+ * `irsend` for sending IR commands
+* Configuration is at: `/etc`
+ * `lircd.conf` contains the human readable names of all the IR commands
+
+```
+begin remote
+
+ name exterity_remote_2
+
+ bits 16
+ flags SPACE_ENC
+ eps 20
+ aeps 200
+
+ header 8800 4400
+ one 550 1650
+ zero 550 550
+ ptrail 550
+ repeat 8800 2200
+ pre_data_bits 16
+ pre_data 0xB5B7
+ gap 38500
+ toggle_bit 0
+ frequency 38000
+
+#! exterity_bit_period 560
+#! exterity_aeps 500
+#! exterity_rmpower_len 66
+#! exterity_rmpower_pattern 16 8 1 3 1 1 1 3 1 3 1 1 1 3 1 1 1 3 1 3 1 1 1 3 1 3 1 1 1 3 1 3 1 3 1 3 1 1 1 3 1 1 1 3 1 3 1 1 1 3 1 1 1 3 1 1 1 3 1 1 1 1 1 3 1 1
+
+ begin codes
+ rm_1 0x45ba
+ rm_2 0x35ca
+ rm_3 0x6d92
+ rm_4 0xc53a
+ rm_5 0xb54a
+ rm_6 0xed12
+ rm_7 0x25da
+ rm_8 0x758a
+ rm_9 0x1de2
+ rm_cancel 0x03fc
+ rm_0 0xf50a
+ rm_menu 0xa55a
+ rm_power 0xad52
+ rm_chup 0x0df2
+ rm_chdown 0x8d72
+ rm_volup 0x5da2
+ rm_voldown 0xdd22
+ rm_up 0x4db2
+ rm_left 0x956a
+ rm_enter 0xcd32
+ rm_right 0xbd42
+ rm_down 0x2dd2
+ rm_mute 0xa35c
+ rm_red 0x837c
+ rm_green 0x43bc
+ rm_yellow 0xc33c
+ rm_blue 0x23dc
+ rm_rewind 0x15ea
+ rm_play 0x55aa
+ rm_pause 0xe51a
+ rm_ff 0x3dc2
+ rm_skipback 0x639c
+ rm_skipfwd 0xe31c
+ rm_stop 0x7d82
+ rm_record 0x659a
+ rm_exterity 0x13ec
+ rm_fn_tv 0x936c
+ rm_fn_home 0x53ac
+ rm_guide 0xd32c
+ rm_subtitle 0x857A
+ rm_info 0x33CC
+ rm_help 0xB34C
+ rm_audio 0x9D62
+ rm_teletext 0xD52A
+ rm_av 0xFD02
+ end codes
+
+end remote
+
+```
+
+
+## Serial Command Interface
+
+* All lines start with `^`
+* All lines end with `!`
+
+Dump of the help text:
+
+
+```
+^help!
+To display a value: ^get:!
+To change a value: ^set: :!
+To display a list of option values: ^dump!
+To send a remote key press: ^send:!
+To send multiple remote key presses: ^msend!:::...:!
+To send a serial command to the TV: ^sendserial:!
+To exit session: ^exit!
+
+Valid options for get and set commands (tag/alternate tag):
+ Name name
+ Location location
+ Groups groups
+ NetworkBootProto dhcp
+ IPAddress
+ Subnet subnetMask
+ Gateway gateway
+ DNSPrimary
+ DNSSecondary
+ TFTPServer
+ StartupMode startupMode
+ currentMode
+ new_display
+ cur_display
+ DoUpgrade upgrade
+ bootfile
+ AdminPassword adminPassword
+ Volume volume
+ product_type productType
+ boardtype boardType
+ boardmod boardMod
+ boardrev boardRevision
+ boardnum boardNumber
+ mac macAddress
+ serial serialNumber
+ cur_webpage currWebpage
+ new_webpage webpage
+ newpage
+ currentChannel
+ currentAVChannel
+ new_channel
+ cur_channel
+ channel_up upChannel
+ channel_down downChannel
+ stop_channel stopChannel
+ play_channel_uri playChannelUri
+ play_channel_number playChannelNumber
+ Play play
+ FastForward fastForward
+ Rewind rewind
+ failover
+ totalresets totalResets
+ remoteresets remoteResets
+ softresets softResets
+ Timeserver timeserver
+ video_scaler scaleDimensions
+ zapper
+ galio
+ serialCommandInterface
+ admin
+ TZ timezone
+ SoftwareVersion softwareVersion
+ softwareDescription
+ mute mute
+ LanguageIso639 prefAudioLang
+ PrefSubtitleLang prefSubtitleLang
+ cur_audiotrack currAudioTrack
+ cur_subtitletrack currSubtitleTrack
+ PlaylistUrl playlistUrl
+ DisplayHD HD
+ UserBitmap splashFile
+ ScreenFormat screenFormat
+ AspectRatio aspectRatio
+ browser.document.default homepage
+ proxySetting
+ proxy
+ proxyIgnore
+ transports.proxy.http.on
+ transports.proxy.http
+ transports.proxy.http.ignore
+ transports.proxy.ftp.on
+ transports.proxy.ftp
+ transports.proxy.ftp.ignore
+ transports.proxy.https.on
+ transports.proxy.https
+ transports.proxy.https.ignore
+ transports.proxy.mailto.on
+ transports.proxy.mailto
+ controller.toolbar.on
+ browserToolbar
+ BrowserSize browserSize
+ TVCtrlType
+ SerialConfig serialConfig
+ StandbyActionsSer standbyActions
+ UnstandbyActionsSer unstandbyActions
+ IRControllerType
+ enableIRReceiver
+ IRMode
+ IROutControllerType
+ StandbyActionsIR standbyActionsIR
+ UnstandbyActionsIR unstandbyActionsIR
+ MasterIRClient masterIRClient
+ VlanEnable vlanEnable
+ VlanNative vlanNative
+ VlanHost vlanHost
+ VlanEth2 vlanEth2
+ VlanEth3 vlanEth3
+ VlanEth4 vlanEth4
+ Speed speed
+ Duplex duplex
+ Autoneg autoneg
+ linkEth
+ rxstatsEth
+ txstatsEth
+ SpeedEth1 speedEth1
+ DuplexEth1 duplexEth1
+ AutonegEth1 autonegEth1
+ linkEth1
+ rxstatsEth1
+ txstatsEth1
+ SpeedEth2 speedEth2
+ DuplexEth2 duplexEth2
+ AutonegEth2 autonegEth2
+ linkEth2
+ rxstatsEth2
+ txstatsEth2
+ SpeedEth3 speedEth3
+ DuplexEth3 duplexEth3
+ AutonegEth3 autonegEth3
+ linkEth3
+ rxstatsEth3
+ txstatsEth3
+ SpeedEth4 speedEth4
+ DuplexEth4 duplexEth4
+ AutonegEth4 autonegEth4
+ linkEth4
+ rxstatsEth4
+ txstatsEth4
+ configureNetworkPorts
+ RemoteLog remoteLogging
+ RemoteLogAddress remoteLogAddress
+ RemoteLogPort remoteLogPort
+ LogLevel remoteLogLevel
+ TVButton
+ HomeButton homeButton
+ GuideButton guideButton
+ rmTVActions
+ rmHomeActions
+ rmGuideActions
+ SAPListener
+ rStaticChannels
+ staticChannels
+ SAPListenAddr
+ XmlChannelListUrl xmlChannelListUrl
+ NfsMountPoints nfsMountPoints
+ UsbMountPoints usbMountPoints
+ HddMountPoints hddMountPoints
+ FailOverStatus failOverStatus
+ add_nfs
+ rem_nfs
+ serialActions
+ FailOverType failOverType
+ FailOverPlaylist failOverPlaylist
+ FailOverBrowser failOverBrowser
+ FailOverMedia failOverMedia
+ NfsMountStatus nfsMountStatus
+ XmlChannelListRefresh xmlChannelListRefresh
+ LocalXmlChannelListRefresh localXmlChannelListRefresh
+ LedStatus ledStatus
+ reboot
+ ScreenSaverTimeout screenSaverTimeout
+ playstream_status playstreamStatus
+ playstream_speed playstreamSpeed
+ SmServerAddress SMServerAddress
+ SmServerPort SMServerPort
+ Subtitles subtitles
+ closedCaptionsDetected
+ closedCaptionChannel
+ savePlaylist
+ clearPlaylist
+ PlaylistDownloadStatus
+ browserEvent
+ doFactoryReset factoryReset
+ exportConfig
+ importConfig
+ BrowserHeap
+ BrowserFlex
+ screenResolution
+ screenFrameRate
+ teletextVBI teletextVBI
+ SNMPD snmpEnable
+ SNMP_RWCOMMUNITY snmpRWCommunity
+ SNMP_ROCOMMUNITY snmpROCommunity
+ usbMount
+ currentScreenResolution
+ lastScreenResolution
+ outputScreenResolution
+ currentScreenFrameRate
+ upTime
+ Date
+ SNMPManager
+ SNMPTrapManager snmpTrapManager
+ usbFileSize
+ usbSpaceLeft
+ hasSwitchChip
+ timeServerInUse
+ devel
+ updateChannelsList
+ stopOnDestroy
+ serialMode
+ serialTVStatus
+ dcardType
+ dcardSerial
+ dcardRev
+ dcardMod
+ hdmiState
+ playLength
+ playPosition
+ 43AspectDisplay
+ SSM_Uri
+ CECAmpenabled
+ CECStandbyenabled
+ CECVolumeChanged
+ CECStatus
+ CECRequestActiveSrc
+ CECSendCmd
+ CECRequestStandby
+ vodFeed
+ uiLang
+ subtitlesShow
+ DeviceType deviceType
+ animateUI
+ webAccess
+ USBStorageAccess
+ remoteMode
+ sourceIPAddr
+ sendKey
+ factoryResetButton
+ securitySetting
+ ApplyPage
+ CAP_VLAN
+ net_stats
+ channel_learning_addrs
+ product_string
+ font_files
+ del_font_files
+ resource_used
+ resource_total
+ stream_type
+ stream_info
+ tv_info
+ decode_state
+ last_decode_state
+ playState
+ edidStatus
+ rtpErrCount
+ current_font
+ bookmarkOne
+ bookmarkTwo
+ bookmarkThree
+ caching
+ tolerance
+ teletext
+ teletextAvailable
+ teletextPageDigit
+ teletextPageDigitReset
+ teletextNavigate
+ teletextPage
+ teletextZoom
+ teletextHoldSubpage
+ Licence
+ videoWallXPosition
+ videoWallYPosition
+ videoWallXSize
+ videoWallYSize
+ vwTopBezelPercent
+ vwLeftBezelPercent
+ vwRightBezelPercent
+ vwBottomBezelPercent
+ importConfigFile
+ exportConfigFile
+ serialPort
+ dnslookup
+ setFuse
+ readJTAG
+ protect
+ hasStarted
+ ConfigVersion
+ SNMPConfigChangeText
+```
+
diff --git a/drivers/exterity/avedia_player/r92xx_spec.cr b/drivers/exterity/avedia_player/r92xx_spec.cr
new file mode 100644
index 00000000000..c3cc878cf89
--- /dev/null
+++ b/drivers/exterity/avedia_player/r92xx_spec.cr
@@ -0,0 +1,23 @@
+DriverSpecs.mock_driver "Exterity::AvediaPlayer::R92xx" do
+ responds("login:")
+ should_send("admin\r\n")
+ should_send("labrador\r\n")
+ should_send("6\r\n")
+ should_send("/usr/bin/serialCommandInterface\r\n", 20.seconds)
+ # this lets the driver know it's successfully connected
+
+ status[:ready].should eq(false)
+ responds("Exterity Control Interface\r")
+ sleep(2)
+ status[:ready].should eq(true)
+
+ exec(:version)
+ responds("^SoftwareVersion:123!\r")
+ sleep(2)
+ status[:version].should eq("123")
+
+ exec(:tv_info)
+ responds("^tv_info:a,b,c,d,e,f,g!\r")
+ sleep(2)
+ status[:tv_info].should eq("a,b,c,d,e,f,g")
+end
diff --git a/drivers/exterity/avedia_player/r93xx.cr b/drivers/exterity/avedia_player/r93xx.cr
new file mode 100644
index 00000000000..9371b43c427
--- /dev/null
+++ b/drivers/exterity/avedia_player/r93xx.cr
@@ -0,0 +1,153 @@
+require "telnet"
+
+module Exterity; end
+
+module Exterity::AvediaPlayer; end
+
+class Exterity::AvediaPlayer::R93xx < PlaceOS::Driver
+ descriptive_name "Exterity Avedia Player (R93xx)"
+ generic_name :IPTV
+ tcp_port 23
+
+ default_settings({
+ max_waits: 100,
+ username: "admin",
+ password: "labrador",
+ })
+
+ @ready : Bool = false
+
+ def on_load
+ new_telnet_client
+ transport.pre_processor { |bytes| @telnet.try &.buffer(bytes) }
+ end
+
+ def connected
+ @ready = false
+ self[:ready] = false
+
+ schedule.every(60.seconds) do
+ logger.debug { "-- Polling Exterity Player" }
+ tv_info
+ end
+ end
+
+ def disconnected
+ # Ensures the buffer is cleared
+ new_telnet_client
+
+ schedule.clear
+ end
+
+ def channel(number : Int32 | String)
+ if number.is_a? Number
+ set :playChannelNumber, number
+ else
+ stream number
+ end
+ end
+
+ def stream(uri : String)
+ set :playChannelUri, uri
+ end
+
+ def dump
+ do_send "^dump!", name: :dump
+ end
+
+ def help
+ do_send "^help!", name: :help
+ end
+
+ def reboot
+ remote :reboot
+ end
+
+ def tv_info
+ get :tv_info
+ end
+
+ def version
+ get :SoftwareVersion
+ end
+
+ def manual(cmd : String)
+ do_send cmd
+ end
+
+ def received(data, task)
+ data = String.new(data).strip
+
+ logger.debug { "Exterity sent #{data}" }
+
+ if @ready
+ # Extract response
+ data.split("!").map(&.strip("^")).each do |resp|
+ process_resp(resp, task)
+ end
+ elsif data =~ /Terminal Control Interface/i
+ @ready = true
+ self[:ready] = true
+ version
+ elsif data =~ /login:/i
+ transport.tokenizer = Tokenizer.new("\r")
+
+ # Login
+ do_send setting(String, :username), wait: false, delay: 2.seconds, priority: 98
+ do_send setting(String, :password), wait: false, delay: 2.seconds, priority: 97
+
+ # We need to disconnect if we don't see the serialCommandInterface after a certain amount of time
+ schedule.in(5.seconds) do
+ if !@ready
+ logger.error { "Exterity connection failed to be ready after 5 seconds. Check username and password." }
+ disconnect
+ end
+ end
+ end
+
+ task.try &.success
+ end
+
+ protected def process_resp(data, task)
+ logger.debug { "Resp details #{data}" }
+
+ parts = data.split ':'
+
+ case parts[0].to_s
+ when "error"
+ if task != nil
+ logger.warn { "Error when requesting: #{task.try &.name}" }
+ else
+ logger.warn { "Error response received" }
+ end
+ when "tv_info"
+ self[:tv_info] = parts[1]
+ when "SoftwareVersion"
+ self[:version] = parts[1]
+ end
+ end
+
+ protected def new_telnet_client
+ @telnet = Telnet.new do |data|
+ transport.send(data)
+ end
+ end
+
+ protected def do_send(command, **options)
+ logger.debug { "requesting #{command}" }
+ send @telnet.not_nil!.prepare(command), **options
+ end
+
+ protected def set(command, data, **options)
+ # options[:name] = :"set_#{command}" unless options[:name]
+ do_send "^set:#{command}:#{data}!", **options
+ end
+
+ protected def remote(cmd, **options)
+ do_send "^send:#{cmd}!", **options
+ end
+
+ protected def get(status, **options)
+ do_send "^get:#{status}!", **options
+ end
+end
diff --git a/drivers/exterity/avedia_player/r93xx_spec.cr b/drivers/exterity/avedia_player/r93xx_spec.cr
new file mode 100644
index 00000000000..aad07511de5
--- /dev/null
+++ b/drivers/exterity/avedia_player/r93xx_spec.cr
@@ -0,0 +1,21 @@
+DriverSpecs.mock_driver "Exterity::AvediaPlayer::R92xx" do
+ responds("login:")
+ should_send("admin\r\n", 3.seconds)
+ should_send("labrador\r\n", 3.seconds)
+
+ # this lets the driver know it's successfully connected
+ status[:ready].should eq(false)
+ responds("Terminal Control Interface\r")
+ sleep(2)
+ status[:ready].should eq(true)
+
+ exec(:version)
+ responds("^SoftwareVersion:123!\r")
+ sleep(2)
+ status[:version].should eq("123")
+
+ exec(:tv_info)
+ responds("^tv_info:a,b,c,d,e,f,g!\r")
+ sleep(2)
+ status[:tv_info].should eq("a,b,c,d,e,f,g")
+end
diff --git a/drivers/extron/matrix.cr b/drivers/extron/matrix.cr
new file mode 100644
index 00000000000..c119b06320f
--- /dev/null
+++ b/drivers/extron/matrix.cr
@@ -0,0 +1,118 @@
+require "./sis"
+
+class Extron::Matrix < PlaceOS::Driver
+ include Extron::SIS
+
+ generic_name :Switcher
+ descriptive_name "Extron matrix switcher"
+ description "Audio-visual signal distribution device"
+ tcp_port SSH_PORT
+
+ def on_load
+ transport.tokenizer = Tokenizer.new DELIMITER
+ end
+
+ def connected
+ send Command['I'], Response::SwitcherInformation do |info|
+ @device_size = info
+ end
+ end
+
+ def disconnected
+ @device_size = nil
+ end
+
+ getter device_size do
+ empty = MatrixSize.new 0, 0
+ SwitcherInformation.new empty, empty
+ end
+
+ alias Outputs = Array(Output)
+
+ alias SignalMap = Hash(Input, Output | Outputs)
+
+ # Connect a signal *input* to an *output* at the specified *layer*.
+ #
+ # `0` may be used as either an input or output to specify a disconnection at
+ # the corresponding signal point. For example, to disconnect input 1 from all
+ # outputs is is currently feeding `switch(1, 0)`.
+ def switch(input : Input, output : Output, layer : SwitchLayer = SwitchLayer::All)
+ send Command[input, '*', output, layer], Response::Tie, &->update_io(Tie)
+ end
+
+ # Connect *input* to all outputs at the specified *layer*.
+ def switch_to(input : Input, layer : SwitchLayer = SwitchLayer::All)
+ send Command[input, '*', layer], Response::Switch, &->update_io(Switch)
+ end
+
+ # Applies a `SignalMap` as a single operation. All included ties will take
+ # simultaneously on the device.
+ def switch_map(map : SignalMap, layer : SwitchLayer = SwitchLayer::All)
+ ties = map.flat_map do |(input, outputs)|
+ if outputs.is_a? Enumerable
+ outputs.each.map { |output| Tie.new input, output, layer }
+ else
+ Tie.new input, outputs, layer
+ end
+ end
+
+ conflicts = ties - ties.uniq(&.output)
+ unless conflicts.empty?
+ raise ArgumentError.new "map contains conflicts for output(s) #{conflicts.join(", ", &.output)}"
+ end
+
+ send Command["\e+Q", ties.map { |tie| [tie.input, '*', tie.output, tie.layer] }, '\r'], Response::Qik do
+ ties.each &->update_io(Tie)
+ end
+ end
+
+ # Send *command* to the device and yield a parsed response to *block*.
+ private def send(command, parser : SIS::Response::Parser(T), &block : T -> _) forall T
+ send command do |data, task|
+ case response = Response.parse data, parser
+ in T
+ task.success block.call response
+ in Error
+ response.retryable? ? task.retry response : task.abort response
+ in Response::ParseError
+ task.abort response
+ end
+ end
+ end
+
+ private def send(command, parser : SIS::Response::Parser(T)) forall T
+ send command, parser, &.itself
+ end
+
+ # Response callback for async responses.
+ def received(data, task)
+ case response = Response.parse data, as: Response::Unsolicited
+ in Tie
+ update_io response
+ in Error, Response::ParseError
+ logger.error { response }
+ in Ok
+ # Nothing to see here, one of the Ignorable responses
+ logger.debug { response }
+ end
+ end
+
+ private def update_io(input : Input, output : Output, layer : SwitchLayer)
+ self["audio#{output}"] = input if layer.includes_audio?
+ self["video#{output}"] = input if layer.includes_video?
+ end
+
+ private def update_io(tie : Tie)
+ update_io tie.input, tie.output, tie.layer
+ end
+
+ # Update exposed driver state to include *switch*.
+ private def update_io(switch : Switch)
+ if switch.layer.includes_video?
+ device_size.video.outputs.times { |o| update_io switch.input, Output.new(o + 1), SwitchLayer::Vid }
+ end
+ if switch.layer.includes_audio?
+ device_size.audio.outputs.times { |o| update_io switch.input, Output.new(o + 1), SwitchLayer::Aud }
+ end
+ end
+end
diff --git a/drivers/extron/matrix_spec.cr b/drivers/extron/matrix_spec.cr
new file mode 100644
index 00000000000..42729aa37ea
--- /dev/null
+++ b/drivers/extron/matrix_spec.cr
@@ -0,0 +1,39 @@
+DriverSpecs.mock_driver "Extron::Matrix" do
+ should_send "I"
+ responds "V8X4 A8X4\r\n"
+
+ switch = exec :switch, input: 3, output: 2
+ should_send "3*2!"
+ responds "Out2 In3 All\r\n"
+ status["video2"].should eq 3
+
+ switch_to = exec :switch_to, input: 2
+ should_send "2*!"
+ responds "In2 All\r\n"
+ status["video1"].should eq 2
+ status["video2"].should eq 2
+ status["video3"].should eq 2
+ status["video4"].should eq 2
+ status["audio1"].should eq 2
+ status["audio2"].should eq 2
+ status["audio3"].should eq 2
+ status["audio4"].should eq 2
+
+ switch_map = exec :switch_map, {1 => [2, 3, 4]}
+ should_send "\e+Q1*2!1*3!1*4!\r"
+ responds "Qik\r\n"
+ status["video2"].should eq 1
+ status["video3"].should eq 1
+ status["video4"].should eq 1
+
+ expect_raises PlaceOS::Driver::RemoteException do
+ conflict = exec :switch_map, {1 => 1, 2 => 1}
+ conflict.get
+ end
+
+ expect_raises PlaceOS::Driver::RemoteException do
+ invalid = exec :switch_to, input: 999
+ responds "E01\r\n"
+ invalid.get
+ end
+end
diff --git a/drivers/extron/sis.cr b/drivers/extron/sis.cr
new file mode 100644
index 00000000000..fcfad4afcd5
--- /dev/null
+++ b/drivers/extron/sis.cr
@@ -0,0 +1,73 @@
+require "./sis/*"
+
+# Implementation, types and utilities for working with the Extron Simple
+# Instruction Set (SIS) device control protocol.
+#
+# This protocol is used for control of all Extron signal distribution,
+# processing and general audio-visual products via SSH, telnet and serial
+# control.
+module Extron::SIS
+ TELNET_PORT = 21
+ SSH_PORT = 22023
+
+ DELIMITER = "\r\n"
+
+ # Illegal characters for use in property names.
+ SPECIAL_CHARS = "+-,@=‘[]{}<>`“;:|?".chars
+
+ # Symbolic type for representating a successfull interactions no useful data.
+ struct Ok; end
+
+ # Device error numbers
+ enum Error
+ InvalidInput = 1
+ InvalidCommand = 10
+ InvalidPresent = 11
+ InvalidOutput = 12
+ InvalidParameter = 13
+ InvalidForConfig = 14
+ Timeout = 17
+ Busy = 22
+ PrivilegesViolation = 24
+ DeviceNotPresent = 25
+ MaxConnectionsExceeded = 26
+ InvalidEventNumber = 27
+ FileNotFound = 28
+
+ def retryable?
+ timeout? || busy?
+ end
+ end
+
+ alias Input = UInt16
+
+ alias Output = UInt16
+
+ # Layers for targetting signal distribution operations.
+ enum SwitchLayer : UInt8
+ All = 0x21 # '!'
+ Aud = 0x24 # '$'
+ Vid = 0x25 # '%'
+ RGB = 0x26 # '&'
+
+ def includes_video?
+ All || Vid || RGB
+ end
+
+ def includes_audio?
+ All || Aud
+ end
+ end
+
+ # Struct for representing a matrix signal path.
+ record Tie, input : Input, output : Output, layer : SwitchLayer
+
+ # Struct for representing a broadcast signal path, or single output switch.
+ record Switch, input : Input, layer : SwitchLayer
+
+ # IO capacity for a switching layer.
+ record MatrixSize, inputs : Input, outputs : Output
+
+ # IO capacity for a full device.
+ record SwitcherInformation, video : MatrixSize, audio : MatrixSize
+end
diff --git a/drivers/extron/sis/command.cr b/drivers/extron/sis/command.cr
new file mode 100644
index 00000000000..87e710b0a4c
--- /dev/null
+++ b/drivers/extron/sis/command.cr
@@ -0,0 +1,34 @@
+# Structure for representing a SIS device command.
+#
+# Commands are composed from a set of *fields*. The contents and types of these
+# are arbitrary, however they must be capable of serialising to an IO.
+struct Extron::SIS::Command(*T)
+ def initialize(*fields : *T)
+ @fields = fields
+ end
+
+ # Serialises `self` in a format suitable for log messages.
+ def to_s(io : IO)
+ io << '‹'
+ to_io io
+ io << '›'
+ end
+
+ # Writes `self` to the passed *io*.
+ def to_io(io : IO, format = IO::ByteFormat::SystemEndian)
+ @fields.each.flatten.each do |field|
+ if field.is_a? Enum
+ io.write_byte field.value
+ else
+ io << field
+ end
+ end
+ end
+
+ # Syntactical suger for `Command` definition. Provides the ability to express
+ # command fields in the same way as `Byte` objects and other similar
+ # collections from the Crystal std lib.
+ macro [](*fields)
+ Extron::SIS::Command.new({{*fields}})
+ end
+end
diff --git a/drivers/extron/sis/response.cr b/drivers/extron/sis/response.cr
new file mode 100644
index 00000000000..79df4123cb2
--- /dev/null
+++ b/drivers/extron/sis/response.cr
@@ -0,0 +1,91 @@
+require "pars"
+
+# Parsers for responses and asynchronous messages originating from Extron SIS
+# devices.
+module Extron::SIS::Response
+ include Pars
+
+ # Parses a response packet with specified *parser*.
+ #
+ # Returns the parser output, a parse error or a device error.
+ def self.parse(data : String, as parser : Parser(T)) forall T
+ (parser | DeviceError | "unhandled device response").parse data
+ end
+
+ # :ditto:
+ def self.parse(data : Bytes, as parser : Parser(T)) forall T
+ parse String.new(data), parser
+ end
+
+ # Parses a number from the input into *type*.
+ private def self.num(type : T.class) forall T
+ Parse.integer.map &->T.new(String)
+ end
+
+ # Parses a word from the input into an enum of *type*.
+ private def self.word_as_enum(type : T.class) forall T
+ Parse.word.map &->T.parse(String)
+ end
+
+ # :nodoc:
+ Delimiter = Parse.string SIS::DELIMITER
+
+ # Parse a full command response as a String. Delimiter is optional as it may
+ # have already been dropped by an upstream tokenizer.
+ Raw = ((Parse.char ^ Delimiter) * (0..) << Delimiter * (0..1)).map &.join
+
+ # Error codes returned from the device.
+ DeviceError = Parse.char('E') >> Parse.integer.map { |e| SIS::Error.new e.to_i }
+
+ # Copyright message shown on connect.
+ Copyright = (Parse.string("(c) Copyright") + Raw).map &.join
+
+ # Part of the copyright banner, but appears on a new line so will tokenize as
+ # as standalone message.
+ Clock = Raw.map { |date| Time.parse_utc date, "%a, %b %d, %Y, %T" }
+
+ # Quick response, occurs following quick tie, or switching interaction from
+ # the device's front panel.
+ Qik = Parse.string("Qik") >> Parse.const(Ok.new)
+
+ # Matrix signal route update.
+ Tie = Parse.do({
+ output <= Parse.string("Out") >> num(Output),
+ _ <= Parse.char(' '),
+ input <= Parse.string("In") >> num(Input),
+ _ <= Parse.char(' '),
+ layer <= word_as_enum(SwitchLayer),
+ Parse.const SIS::Tie.new input, output, layer,
+ })
+
+ # Broadcast or single output route update.
+ Switch = Parse.do({
+ input <= Parse.string("In") >> num(Input),
+ _ <= Parse.char(' '),
+ layer <= word_as_enum(SwitchLayer),
+ Parse.const SIS::Switch.new input, layer,
+ })
+
+ MatrixSize = Parse.do({
+ inputs <= num(Input),
+ _ <= Parse.char('X'),
+ outputs <= num(Output),
+ Parse.const SIS::MatrixSize.new inputs, outputs,
+ })
+
+ SwitcherInformation = Parse.do({
+ _ <= Parse.char('V'),
+ video <= MatrixSize,
+ _ <= Parse.char(' '),
+ _ <= Parse.char('A'),
+ audio <= MatrixSize,
+ Parse.const SIS::SwitcherInformation.new video, audio,
+ })
+
+ # Parses for device messages that can be safely ignored - these exist mainly
+ # to flush initial connect banners
+ Ignorable = (Copyright | Clock) >> Parse.const(Ok.new)
+
+ # Async messages that can be expected outside of a command -> response flow.
+ Unsolicited = DeviceError | Tie | Ignorable
+end
diff --git a/drivers/extron/sis_spec.cr b/drivers/extron/sis_spec.cr
new file mode 100644
index 00000000000..e61c580b54e
--- /dev/null
+++ b/drivers/extron/sis_spec.cr
@@ -0,0 +1,104 @@
+require "spec"
+require "./sis"
+
+include Extron::SIS
+
+describe Command do
+ it "forms a Command from arbitrary field types" do
+ command = Command.new 42, 'a', "foo"
+ end
+
+ it "serialises the command to an IO" do
+ command = Command[1, '*', 2, SwitchLayer::All]
+ io = IO::Memory.new
+ io.write_bytes command
+ io.to_s.should eq("1*2!")
+ end
+
+ it "provides a string representation suitable for logging" do
+ command = Command[1, '*', 2, SwitchLayer::All]
+ command.to_s.should eq("‹1*2!›")
+ end
+
+ it "flattens nested fields" do
+ routes = [
+ [1, '*', 2, SwitchLayer::All],
+ [3, '*', 4, SwitchLayer::All],
+ ]
+ command = Command["\e+Q", routes, '\r']
+ io = IO::Memory.new
+ io.write_bytes command
+ io.to_s.should eq("\e+Q1*2!3*4!\r")
+ end
+end
+
+describe Response do
+ describe Response::DeviceError do
+ it "parses to a SIS::Error" do
+ error = Response::DeviceError.parse "E17"
+ error.should eq(Error::Timeout)
+ end
+ end
+
+ describe Response::Copyright do
+ it "parses and provides the full banner" do
+ message = "(c) Copyright YYYY, Extron Electronics, Model Name, Vx.xx, nn-nnnn-nn"
+ parsed = Response::Copyright.parse message
+ parsed.should be_a(String)
+ parsed.should eq(message)
+ end
+
+ it "does not parse other messages" do
+ parsed = Response::Copyright.parse "foo"
+ parsed.should be_a(Response::ParseError)
+ end
+ end
+
+ describe Response::Clock do
+ it "parses" do
+ clock = "Fri, Feb 13, 2009, 23:31:30"
+ parsed = Response::Clock.parse clock
+ parsed.as(Time).to_unix.should eq(1234567890)
+ end
+ end
+
+ describe Response::Tie do
+ it "parses" do
+ tie = Response::Tie.parse "Out2 In1 All"
+ tie.should be_a Tie
+ tie = tie.as Tie
+ tie.input.should eq(1)
+ tie.output.should eq(2)
+ tie.layer.should eq(SwitchLayer::All)
+ end
+ end
+
+ describe Response::Switch do
+ it "parses" do
+ tie = Response::Switch.parse "In1 All"
+ tie.should be_a Switch
+ tie = tie.as Switch
+ tie.input.should eq(1)
+ tie.layer.should eq(SwitchLayer::All)
+ end
+ end
+
+ describe Response::SwitcherInformation do
+ it "parses" do
+ info = Response::SwitcherInformation.parse "V1X2 A3X4"
+ info.should be_a SwitcherInformation
+ info = info.as SwitcherInformation
+ info.video.inputs.should eq 1
+ info.video.outputs.should eq 2
+ info.audio.inputs.should eq 3
+ info.audio.outputs.should eq 4
+ end
+ end
+
+ describe ".parse" do
+ it "builds a parser that includes device errors" do
+ resp = Response.parse "Out4 In2 Aud", as: Response::Tie
+ typeof(resp).should eq (Tie | Error | Response::ParseError)
+ end
+ end
+end
diff --git a/drivers/floorsense/booking-sync-overview.md b/drivers/floorsense/booking-sync-overview.md
new file mode 100644
index 00000000000..75cf84dd96c
--- /dev/null
+++ b/drivers/floorsense/booking-sync-overview.md
@@ -0,0 +1,87 @@
+# Floorsense Boooking Sync
+
+This is a quick overview of how PlaceOS interacts with Floorsense to enable floorsense features such as desk check-in.
+
+
+## System layout
+
+You'll need the following drivers and corresponding modules added to a system to sync bookings between PlaceOS and Floorsense
+
+* `PlaceOS Staff API` this provides access to PlaceOS API functions
+* `Floorsense Desk Tracking` this implements the floorsense API functions
+* `Floorsense Bookings Sync` this keeps the two systems in sync
+
+
+## Sync Configuration
+
+Plan IDs in Floorsense need to be mapped to zones in PlaceOS, this configuration should be configured in the `Floorsense Bookings Sync` driver
+
+```yaml
+
+floor_mappings:
+ '1':
+ building_id: zone-GAf3dfZq8
+ level_id: zone-GAf5RN-ne
+ name: Building Name - Level 16
+
+# Timezone of the building
+time_zone: Australia/Brisbane
+
+# time in seconds between polling for changes
+poll_rate: 3
+
+```
+
+NOTE:: this assumes that desk ids in Floorsense and PlaceOS match, which is desirable from a maintenance and support standpoint.
+
+There are some additional settings for adding prefixes to desk names, but these should be avoided if at all possible
+
+
+## Driver operation
+
+The driver monitors a couple of things at once
+
+* monitors for real-time changes occurring in PlaceOS Staff API
+ * changes in booking state, bookings added or removed
+* polls PlaceOS bookings periodically in case a booking was missed
+* polls Floorsense change log rapidly to detect new ad-hoc bookings, booking check-outs or check-ins
+
+PlaceOS bookings are added to floorsense 1 day before the booking,
+so todays and tomorrows bookings are kept in sync.
+
+
+### PlaceOS Booking Created
+
+1. Sync determines that a new Floorsense booking needs to be created
+2. Checks the user exists in Floorsense and adds them if they don't
+3. Checks the users card number is correct in Floorsense, updates this if not
+4. Creates the booking in Floorsense
+
+
+### PlaceOS Booking Deleted
+
+1. Sync determines that the Floorsense booking needs to be removed
+2. The Floorsense booking is released
+
+
+### Floorsense Booking Created
+
+1. Checks that the booking is an ad-hoc booking
+2. Attempts to locate the equivalent user in PlaceOS
+3. Creates the booking on behalf of the user in PlaceOS
+
+Only ad-hoc bookings are created in PlaceOS.
+All other booking types will be removed automatically as part of the sync if there are no matching PlaceOS bookings.
+
+
+### Floorsense Booking Checked-in
+
+2. Attempts to locate the booking in PlaceOS
+3. Marks the booking as checked-in
+
+
+### Floorsense Booking Checked-out
+
+2. Attempts to locate the booking in PlaceOS
+3. Changes the end time of the booking to now (so the booking has effectively ended)
+4. This has the effect of freeing the desk
diff --git a/drivers/floorsense/bookings_sync.cr b/drivers/floorsense/bookings_sync.cr
new file mode 100644
index 00000000000..7b300d6a59a
--- /dev/null
+++ b/drivers/floorsense/bookings_sync.cr
@@ -0,0 +1,552 @@
+module Floorsense; end
+
+require "uri"
+require "json"
+require "oauth2"
+require "placeos-driver/interface/locatable"
+require "./models"
+
+class Floorsense::BookingsSync < PlaceOS::Driver
+ descriptive_name "Floorsense Bookings Sync"
+ generic_name :FloorsenseBookingSync
+ description %(syncs PlaceOS bookings with floorsense booking system)
+
+ accessor floorsense : Floorsense_1
+ accessor staff_api : StaffAPI_1
+
+ default_settings({
+ floor_mappings: {
+ "planid": {
+ building_id: "zone-building",
+ level_id: "zone-level",
+ name: "friendly name for documentation",
+ },
+ },
+ time_zone: "GMT",
+ poll_rate: 3,
+ key_prefix: "desk-",
+ strip_leading_zero: true,
+ zero_padding_size: 7,
+ })
+
+ @floor_mappings : Hash(String, NamedTuple(building_id: String?, level_id: String)) = {} of String => NamedTuple(building_id: String?, level_id: String)
+ # Level zone => plan_id
+ @zone_mappings : Hash(String, String) = {} of String => String
+ # Level zone => building_zone
+ @building_mappings : Hash(String, String?) = {} of String => String?
+
+ @booking_type : String = "desk"
+ @key_prefix : String = "desk-"
+ @strip_leading_zero : Bool = true
+ @zero_padding_size : Int32 = 7
+ @poll_rate : Time::Span = 3.seconds
+ @time_zone : Time::Location = Time::Location.load("GMT")
+
+ @sync_lock = Mutex.new
+
+ def on_load
+ monitor("staff/booking/changed") do |_subscription, payload|
+ logger.debug { "received booking changed event #{payload}" }
+ booking_changed(Booking.from_json(payload))
+ end
+ on_update
+ end
+
+ def on_update
+ @key_prefix = setting?(String, :key_prefix) || ""
+ @booking_type = setting?(String, :booking_type).presence || "desk"
+ @strip_leading_zero = setting?(Bool, :strip_leading_zero) || false
+ @zero_padding_size = setting?(Int32, :zero_padding_size) || 7
+
+ @booking_type = setting?(String, :booking_type).presence || "desk"
+ @poll_rate = (setting?(Int32, :poll_rate) || 3).seconds
+
+ @floor_mappings = setting(Hash(String, NamedTuple(building_id: String?, level_id: String)), :floor_mappings)
+ @floor_mappings.each do |plan_id, details|
+ level = details[:level_id]
+ @building_mappings[level] = details[:building_id]
+ @zone_mappings[level] = plan_id
+ end
+
+ time_zone = setting?(String, :calendar_time_zone).presence || "GMT"
+ @time_zone = Time::Location.load(time_zone)
+
+ schedule.clear
+ schedule.in(500.milliseconds) { @sync_lock.synchronize { check_floorsense_log } }
+ schedule.every(@poll_rate) { @sync_lock.synchronize { check_floorsense_log } }
+
+ # between polls, sync the bookings
+ schedule.in(@poll_rate / 2) do
+ schedule.every(@poll_rate * 10) { sync_bookings }
+ sync_bookings
+ end
+ end
+
+ # ===================================
+ # Desk ID manipulation
+ # ===================================
+ def to_place_asset_id(key : String)
+ key = key.lstrip('0') if @strip_leading_zero
+ "#{@key_prefix}#{key}"
+ end
+
+ def to_floor_key(asset_id : String)
+ asset_id = asset_id.lstrip(@key_prefix) if @key_prefix.presence
+ asset_id = asset_id.rjust(@zero_padding_size, '0') if @strip_leading_zero
+ asset_id
+ end
+
+ # ===================================
+ # Polling for events
+ # ===================================
+ @last_event_id : Int64? = nil
+ @last_event_at : Int64 = 0_i64
+
+ def check_floorsense_log : Nil
+ last_event_id = @last_event_id
+ if last_event_id.nil?
+ recent = floorsense.event_log({49, 50, 53}).get.as_a
+ if !recent.empty?
+ last = recent.last
+ @last_event_id = last["eventid"].as_i64
+ @last_event_at = last["eventtime"].as_i64
+ end
+ return
+ end
+
+ events = Array(LogEntry).from_json floorsense.event_log(
+ codes: {49, 50, 53},
+ after: @last_event_at,
+ limit: 500
+ ).get.to_json
+
+ # it returns all the events that happened at the time specified
+ # some of these might have happened before this event id
+ # and it'll always return the last seen event id
+ events.reject! { |event| event.eventid <= last_event_id }
+ return if events.empty?
+
+ logger.debug { "parsing floorsense event log, #{events.size} new events" }
+
+ @last_event_id = events.last.eventid
+ events.each do |event|
+ begin
+ booking = BookingStatus.from_json floorsense.get_booking(event.bkid).get.to_json
+ floor_details = @floor_mappings[booking.planid.to_s]?
+ next unless floor_details
+
+ case event.code
+ when 49 # BOOKING_CREATE (ad-hoc?)
+ next if booking.booking_type != "adhoc"
+
+ user_email = booking.user.not_nil!.email.try &.downcase
+
+ if user_email.nil?
+ logger.warn { "no user email defined for floorsense user #{booking.user.not_nil!.name}" }
+ next
+ end
+
+ user = staff_api.user(user_email).get
+ user_id = user["id"]
+ user_name = user["name"]
+
+ logger.debug { "new floorsense booking found #{booking}" }
+
+ staff_api.create_booking(
+ booking_start: booking.start,
+ booking_end: booking.finish,
+ time_zone: @time_zone.to_s,
+ booking_type: @booking_type,
+ asset_id: to_place_asset_id(booking.key),
+ user_id: user_id,
+ user_email: user_email,
+ user_name: user_name,
+ zones: [floor_details[:building_id]?, floor_details[:level_id]].compact,
+ checked_in: true,
+ extension_data: {
+ floorsense_id: event.bkid,
+ },
+ ).get
+ when 50 # BOOKING_RELEASE (booking ended)
+ # ignore bookings that were cancelled outside of today
+ next if booking.released >= booking.finish || booking.released <= booking.start
+
+ # find placeos booking
+ if place_booking = get_place_booking(booking, floor_details)
+ # change the placeos end time if the booking has started
+ staff_api.update_booking(
+ booking_id: place_booking.id,
+ booking_end: booking.released
+ ).get
+ else
+ logger.warn { "no booking found for released booking #{booking.booking_id}" }
+ end
+ when 51 # BOOKING_UPDATE (booking changed)
+ when 52 # BOOKING_ACTIVATE (advanced booking - i.e. tomorrow)
+ when 53 # BOOKING_CONFIRM (checked in)
+ # find placeos booking (should only fail here for adhoc which are already checked in)
+ begin
+ if desc = booking.desc
+ place_booking = Booking.from_json staff_api.get_booking(desc.to_i64).get.to_json
+ staff_api.booking_check_in(place_booking.id, booking.confirmed)
+ end
+ rescue ArgumentError
+ # was an adhoc booking
+ end
+ end
+ rescue error
+ logger.warn(exception: error) { "while processing #{event.eventid}\n#{event.inspect}" }
+ end
+ end
+ end
+
+ protected def get_place_booking(freespace_booking, floor_details) : Booking?
+ if desc = freespace_booking.desc
+ Booking.from_json staff_api.get_booking(desc.to_i64).get.to_json
+ else
+ search_place_booking(freespace_booking, floor_details)
+ end
+ rescue ArgumentError
+ # in case the description was unexpectedly not an int64 (adhoc for instance)
+ search_place_booking(freespace_booking, floor_details)
+ end
+
+ protected def search_place_booking(freespace_booking, floor_details)
+ user_email = freespace_booking.user.not_nil!.email.try &.downcase
+
+ if user_email.nil?
+ logger.warn { "no user email defined for floorsense user #{freespace_booking.user.not_nil!.name}" }
+ return nil
+ end
+
+ bookings = staff_api.query_bookings(
+ type: @booking_type,
+ period_start: freespace_booking.start,
+ period_end: freespace_booking.finish,
+ zones: {floor_details[:level_id]},
+ email: user_email
+ ).get.as_a
+
+ bookings.compact_map { |book|
+ booking = Booking.from_json(book.to_json)
+ booking.rejected ? nil : booking
+ }.first?
+ end
+
+ # ===================================
+ # Monitoring desk bookings
+ # ===================================
+ protected def booking_changed(event)
+ return unless event.booking_type == @booking_type
+ matching_zones = @zone_mappings.keys & event.zones
+ return if matching_zones.empty?
+
+ logger.debug { "booking event is in a matching zone" }
+
+ sync_floor(matching_zones.first)
+ end
+
+ def sync_bookings
+ @zone_mappings.keys.each { |zone_id| sync_floor(zone_id) }
+ end
+
+ def sync_floor(zone : String)
+ @sync_lock.synchronize { do_sync_floor(zone) }
+ end
+
+ protected def do_sync_floor(zone : String)
+ plan_id = @zone_mappings[zone]?
+ if plan_id.nil?
+ logger.warn { "unknown plan ID for zone #{zone}" }
+ return 0
+ end
+ floor_details = @floor_mappings[plan_id]
+
+ logger.debug { "syncing zone #{zone}, plan-id #{plan_id}" }
+
+ place_bookings = placeos_bookings(zone)
+ sense_bookings = floorsense_bookings(zone)
+
+ adhoc = [] of BookingStatus
+ other = [] of BookingStatus
+
+ sense_bookings.each do |booking|
+ if booking.booking_type == "adhoc"
+ adhoc << booking
+ else
+ other << booking
+ end
+ end
+
+ logger.debug { "found #{adhoc.size} adhoc bookings" }
+
+ place_booking_checked = Set(String).new
+ release_floor_bookings = [] of BookingStatus
+ release_place_bookings = [] of Tuple(Booking, Int64)
+ create_place_bookings = [] of BookingStatus
+ create_floor_bookings = [] of Booking
+
+ time_now = 2.minutes.from_now.to_unix
+
+ # adhoc bookings need to be added to PlaceOS
+ adhoc.each do |floor_booking|
+ found = false
+ place_bookings.each do |booking|
+ # match using extenstion data
+ if (ext_data = booking.extension_data) && (floor_id = ext_data["floorsense_id"]?.try(&.as_s)) && floor_id == floor_booking.booking_id
+ found = true
+ place_booking_checked << booking.id.to_s
+ else
+ next
+ end
+
+ if (booking.rejected || booking.booking_end != floor_booking.finish) && floor_booking.released == 0_i64
+ release_floor_bookings << floor_booking
+ elsif floor_booking.released > 0_i64 && floor_booking.released != booking.booking_end && !booking.rejected
+ # need to change end time of this booking
+ release_place_bookings << {booking, floor_booking.released}
+ end
+
+ break
+ end
+
+ if !found && floor_booking.released == 0_i64
+ create_place_bookings << floor_booking
+ end
+ end
+
+ logger.debug { "need to sync #{create_place_bookings.size} adhoc bookings, release #{release_place_bookings.size} bookings" }
+
+ # what bookings need to be added to floorsense
+ place_bookings.each do |booking|
+ booking_id = booking.id.to_s
+ next if place_booking_checked.includes?(booking_id)
+ place_booking_checked << booking_id
+
+ next if time_now >= booking.booking_end
+
+ found = false
+ other.each do |floor_booking|
+ next unless floor_booking.desc == booking_id
+ found = true
+
+ # TODO:: check for booking changes?
+ # we currently are not and probably shouldn't be moving bookings to different days
+
+ if (booking.rejected || booking.booking_end != floor_booking.finish) && floor_booking.released == 0_i64
+ release_floor_bookings << floor_booking
+ elsif floor_booking.released > 0_i64 && floor_booking.released != booking.booking_end && !booking.rejected
+ # need to change end time of this booking
+ release_place_bookings << {booking, floor_booking.released}
+ end
+
+ break
+ end
+
+ create_floor_bookings << booking unless found || booking.rejected
+ end
+
+ other.each do |floor_booking|
+ release_floor_bookings << floor_booking unless place_booking_checked.includes?(floor_booking.desc)
+ end
+
+ logger.debug { "need to create #{create_floor_bookings.size} bookings, release #{release_floor_bookings.size} in floorsense" }
+
+ # update floorsense
+ local_floorsense = floorsense
+ release_floor_bookings.each { |floor_booking| local_floorsense.release_booking(floor_booking.booking_id) }
+
+ create_floor_bookings.each do |booking|
+ floor_user = begin
+ get_floorsense_user(booking.user_id)
+ rescue error
+ logger.warn(exception: error) { "unable to find or create user #{booking.user_id} (#{booking.user_email}) in floorsense" }
+ next
+ end
+
+ # We need a floorsense user to own the booking
+ # floor_user = local_floorsense.user_list(booking.user_email).get.as_a.first?
+
+ local_floorsense.create_booking(
+ user_id: floor_user,
+ plan_id: plan_id,
+ key: to_floor_key(booking.asset_id),
+ description: booking.id.to_s,
+ starting: booking.booking_start < time_now ? 5.minutes.ago.to_unix : booking.booking_start,
+ ending: booking.booking_end
+ )
+ end
+
+ logger.debug { "floorsense bookings created" }
+
+ # update placeos
+ local_staff_api = staff_api
+ release_place_bookings.each do |booking, released|
+ local_staff_api.update_booking(
+ booking_id: booking.id,
+ booking_end: released
+ )
+ end
+
+ logger.debug { "#{release_place_bookings.size} place bookings released" }
+
+ create_place_bookings.each do |booking|
+ user_id = booking.user.not_nil!.desc
+ user_email = booking.user.not_nil!.email.try &.downcase
+
+ if user_id.presence.nil? && user_email.presence.nil?
+ logger.warn { "no user id or email defined for floorsense user #{booking.user.not_nil!.name}" }
+ next
+ end
+
+ user = begin
+ local_staff_api.user(user_id.presence || user_email).get
+ rescue error
+ logger.warn(exception: error) { "floorsense user #{user_email} not found in placeos" }
+ next
+ end
+ user_id = user["id"]
+ user_name = user["name"]
+ user_email = user["email"]
+
+ local_staff_api.create_booking(
+ booking_start: booking.start,
+ booking_end: booking.finish,
+ booking_type: @booking_type,
+ asset_id: to_place_asset_id(booking.key),
+ user_id: user_id,
+ user_email: user_email,
+ user_name: user_name,
+ zones: [floor_details[:building_id]?, floor_details[:level_id]].compact,
+ extension_data: {
+ floorsense_id: booking.booking_id,
+ },
+ )
+ end
+
+ logger.debug { "#{create_place_bookings.size} adhoc place bookings created" }
+
+ # number of bookings checked
+ place_bookings.size + adhoc.size
+ end
+
+ # ===================================
+ # Sync Users
+ # ===================================
+ def get_floorsense_user(placeos_user_id : String) : String
+ users = floorsense.user_list(description: placeos_user_id).get.as_a
+ user_id = users.first?.try(&.[]("uid").as_s)
+
+ # We might need to create a
+ card_number = nil
+ begin
+ place_user = staff_api.user(placeos_user_id).get
+ name = place_user["name"].as_s
+ email = place_user["email"].as_s
+ card_number = place_user["card_number"]?.try(&.as_s)
+
+ # Add the card number to the user
+ user_id ||= floorsense.create_user(name, email, placeos_user_id).get["uid"].as_s
+ rescue error
+ if user_id
+ # if we have a user id, lets just return it
+ # a staff_api outage shouldn't prevent this from working
+ return user_id
+ else
+ raise error
+ end
+ end
+
+ if user_id && card_number && !card_number.empty?
+ ensure_card_synced(card_number, user_id)
+ end
+
+ user_id.not_nil!
+ end
+
+ protected def ensure_card_synced(card_number : String, user_id : String) : Nil
+ existing_user = begin
+ floorsense.get_rfid(card_number).get["uid"].as_s
+ rescue
+ nil
+ end
+
+ if existing_user != user_id
+ floorsense.delete_rfid(card_number)
+ floorsense.create_rfid(user_id, card_number)
+ end
+ rescue error
+ logger.warn(exception: error) { "failed to sync card number #{card_number} for user #{user_id}" }
+ end
+
+ # ===================================
+ # Booking Queries
+ # ===================================
+ def floorsense_bookings(zone_id : String)
+ logger.debug { "querying floorsense bookings in zone #{zone_id}" }
+
+ plan_id = @zone_mappings[zone_id]?
+ return [] of BookingStatus unless plan_id
+
+ current = [] of BookingStatus
+ start_of_day = Time.local(@time_zone).at_beginning_of_day
+ tomorrow_night = (start_of_day.at_end_of_day + 1.hour).at_end_of_day
+
+ raw_bookings = floorsense.bookings(plan_id, start_of_day.to_unix, tomorrow_night.to_unix).get.to_json
+ Hash(String, Array(BookingStatus)).from_json(raw_bookings).each_value do |bookings|
+ current << bookings.first unless bookings.empty?
+ end
+ current
+ end
+
+ def placeos_bookings(zone_id : String)
+ start_of_day = Time.local(@time_zone).at_beginning_of_day
+ tomorrow_night = (start_of_day.at_end_of_day + 1.hour).at_end_of_day
+
+ bookings = staff_api.query_bookings(
+ type: @booking_type,
+ period_start: start_of_day.to_unix,
+ period_end: tomorrow_night.to_unix,
+ zones: {zone_id}
+ ).get.as_a
+
+ bookings.map { |book| Booking.from_json(book.to_json) }
+ end
+
+ class Booking
+ include JSON::Serializable
+
+ # This is to support events
+ property action : String?
+
+ property id : Int64
+ property booking_type : String
+ property booking_start : Int64
+ property booking_end : Int64
+ property timezone : String?
+
+ # events use resource_id instead of asset_id
+ property asset_id : String?
+ property resource_id : String?
+
+ def asset_id : String
+ (@asset_id || @resource_id).not_nil!
+ end
+
+ property user_id : String
+ property user_email : String
+ property user_name : String
+
+ property zones : Array(String)
+
+ property checked_in : Bool?
+ property rejected : Bool?
+
+ property extension_data : JSON::Any?
+
+ def in_progress?
+ now = Time.utc.to_unix
+ now >= @booking_start && now < @booking_end
+ end
+ end
+end
diff --git a/drivers/floorsense/desks.cr b/drivers/floorsense/desks.cr
new file mode 100644
index 00000000000..6c3442fbc20
--- /dev/null
+++ b/drivers/floorsense/desks.cr
@@ -0,0 +1,446 @@
+require "uri"
+require "jwt"
+require "./models"
+
+module Floorsense; end
+
+# Documentation:
+# https://apiguide.smartalock.com/
+# https://documenter.getpostman.com/view/8843075/SVmwvctF?version=latest#3bfbb050-722d-4433-889a-8793fa90af9c
+
+class Floorsense::Desks < PlaceOS::Driver
+ # Discovery Information
+ generic_name :Floorsense
+ descriptive_name "Floorsense Desk Tracking"
+
+ uri_base "https://_your_subdomain_.floorsense.com.au"
+
+ default_settings({
+ username: "srvc_acct",
+ password: "password!",
+ })
+
+ @username : String = ""
+ @password : String = ""
+ @auth_token : String = ""
+ @auth_expiry : Time = 1.minute.ago
+ @user_cache : Hash(String, User) = {} of String => User
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @username = URI.encode_www_form setting(String, :username)
+ @password = URI.encode_www_form setting(String, :password)
+ end
+
+ def expire_token!
+ @auth_expiry = 1.minute.ago
+ end
+
+ def token_expired?
+ now = Time.utc
+ @auth_expiry < now
+ end
+
+ def get_token
+ return @auth_token unless token_expired?
+
+ response = post("/restapi/login", body: "username=#{@username}&password=#{@password}", headers: {
+ "Content-Type" => "application/x-www-form-urlencoded",
+ "Accept" => "application/json",
+ })
+
+ data = response.body.not_nil!
+ logger.debug { "received login response #{data}" }
+
+ if response.success?
+ resp = AuthResponse.from_json(data)
+ token = resp.info.not_nil!.token
+ payload, _ = JWT.decode(token, verify: false, validate: false)
+ @auth_expiry = (Time.unix payload["exp"].as_i64) - 5.minutes
+ @auth_token = "Bearer #{token}"
+ else
+ case response.status_code
+ when 401
+ resp = AuthResponse.from_json(data)
+ logger.warn { "#{resp.message} (#{resp.code})" }
+ else
+ logger.error { "authentication failed with HTTP #{response.status_code}" }
+ end
+ raise "failed to obtain access token"
+ end
+ end
+
+ def floors
+ token = get_token
+ uri = "/restapi/floorplan-list"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ check_response FloorsResponse.from_json(response.body.not_nil!)
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def desks(plan_id : String | Int32)
+ token = get_token
+ uri = "/restapi/floorplan-desk?planid=#{plan_id}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ check_response DesksResponse.from_json(response.body.not_nil!)
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def bookings(plan_id : String, period_start : Int64? = nil, period_end : Int64? = nil)
+ token = get_token
+ period_start ||= Time.utc.to_unix
+ period_end ||= 15.minutes.from_now.to_unix
+ uri = "/restapi/floorplan-booking?planid=#{plan_id}&start=#{period_start}&finish=#{period_end}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ bookings_map = check_response(BookingsResponse.from_json(response.body.not_nil!))
+ bookings_map.each do |_id, bookings|
+ # get the user information
+ bookings.each { |booking| booking.user = get_user(booking.uid) }
+ end
+ bookings_map
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def get_booking(booking_id : String | Int64)
+ token = get_token
+ uri = "/restapi/booking?bkid=#{booking_id}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ booking = check_response BookingResponse.from_json(response.body.not_nil!)
+ booking.user = get_user(booking.uid)
+ booking
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def create_booking(
+ user_id : String | Int64,
+ plan_id : String | Int32,
+ key : String,
+ description : String? = nil,
+ starting : Int64? = nil,
+ ending : Int64? = nil,
+ time_zone : String? = nil,
+ booking_type : String = "advance"
+ )
+ desks_on_plan = desks(plan_id)
+ desk = desks_on_plan.find { |entry| entry.key == key }
+
+ raise "could not find desk #{key} on plan #{plan_id}" unless desk
+
+ token = get_token
+ uri = "/restapi/booking-create"
+
+ now = time_zone ? Time.local(Time::Location.load(time_zone)) : Time.local
+ starting ||= now.at_beginning_of_day.to_unix
+ ending ||= now.at_end_of_day.to_unix
+
+ response = post(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ "Content-Type" => "application/x-www-form-urlencoded",
+ }, body: URI::Params.build { |form|
+ form.add("uid", user_id.to_s)
+ form.add("cid", desk.cid.to_s)
+ form.add("key", key)
+ form.add("bktype", booking_type)
+ form.add("desc", description.not_nil!) if description
+ form.add("start", starting.to_s)
+ form.add("finish", ending.to_s)
+ form.add("confexpiry", ending.to_s)
+ })
+
+ if response.success?
+ booking = check_response BookingResponse.from_json(response.body.not_nil!)
+ booking.user = get_user(booking.uid)
+ booking
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def release_booking(booking_id : String | Int64)
+ token = get_token
+ uri = "/restapi/booking-release"
+
+ response = post(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ "Content-Type" => "application/x-www-form-urlencoded",
+ }, body: URI::Params.build(&.add("bkid", booking_id.to_s)))
+
+ if response.success?
+ true
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def create_user(
+ name : String,
+ email : String,
+ description : String? = nil,
+ extid : String? = nil,
+ pin : String? = nil,
+ usertype : String = "user"
+ )
+ token = get_token
+ uri = "/restapi/user-create"
+
+ response = post(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ "Content-Type" => "application/x-www-form-urlencoded",
+ }, body: URI::Params.build { |form|
+ form.add("name", name)
+ form.add("email", email)
+ form.add("desc", description.not_nil!) if description
+ form.add("pin", pin.not_nil!) if pin
+ form.add("extid", extid.not_nil!) if extid
+ form.add("usertype", "user")
+ })
+
+ if response.success?
+ user = check_response UserResponse.from_json(response.body.not_nil!)
+ @user_cache[user.uid] = user
+ user
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def create_rfid(
+ user_id : String,
+ card_number : String,
+ description : String? = nil
+ )
+ token = get_token
+ uri = "/restapi/rfid-create"
+
+ response = post(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ "Content-Type" => "application/x-www-form-urlencoded",
+ }, body: URI::Params.build { |form|
+ form.add("uid", user_id)
+ form.add("csn", card_number)
+ form.add("desc", description.not_nil!) if description
+ })
+
+ if response.success?
+ check_response GenericResponse.from_json(response.body.not_nil!)
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def delete_rfid(card_number : String)
+ token = get_token
+ uri = "/restapi/rfid-delete"
+
+ response = post(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ "Content-Type" => "application/x-www-form-urlencoded",
+ }, body: URI::Params.build { |form|
+ form.add("csn", card_number)
+ })
+
+ if response.success?
+ true
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def get_rfid(card_number : String)
+ token = get_token
+ uri = "/restapi/rfid?csn=#{card_number}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ check_response RFIDResponse.from_json(response.body.not_nil!)
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def get_user(user_id : String)
+ existing = @user_cache[user_id]?
+ return existing if existing
+
+ token = get_token
+ uri = "/restapi/user?uid=#{user_id}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ user = check_response UserResponse.from_json(response.body.not_nil!)
+ @user_cache[user_id] = user
+ user
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def user_list(email : String? = nil, name : String? = nil, description : String? = nil)
+ query = URI::Params.build { |form|
+ form.add("email", email.not_nil!) if email
+ form.add("name", name.not_nil!) if name
+ form.add("desc", description.not_nil!) if description
+ }
+
+ token = get_token
+ uri = "/restapi/user-list?#{query}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ check_response UsersResponse.from_json(response.body.not_nil!)
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def event_log(codes : Array(String | Int32), event_id : Int64? = nil, after : Int64? = nil, limit : Int32 = 1)
+ token = get_token
+ query = URI::Params.build { |form|
+ form.add("codes", codes.join(",", &.to_s))
+ form.add("after", after.not_nil!.to_s) if after
+ form.add("event_id", event_id.not_nil!.to_s) if event_id
+ form.add("limit", limit.to_s)
+ }
+
+ uri = "/restapi/event-log?#{query}"
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ # Responses are not returned sorted, we want the oldest event first
+ # oldest first as we want to process events in the order that they happen
+ check_response(LogResponse.from_json(response.body.not_nil!)).sort do |a, b|
+ if a.eventtime == b.eventtime
+ a.eventid <=> b.eventid
+ else
+ a.eventtime <=> b.eventtime
+ end
+ end
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ def at_location(controller_id : String, desk_key : String)
+ token = get_token
+ uri = "/restapi/user-locate?cid=#{controller_id}&desk_key=#{desk_key}"
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ logger.debug { "at_location response: #{response.body}" }
+
+ if response.success?
+ users = check_response UsersResponse.from_json(response.body.not_nil!)
+ users.first?
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ @[Security(Level::Support)]
+ def clear_user_cache!
+ @user_cache.clear
+ end
+
+ def locate(key : String, controller_id : String? = nil)
+ token = get_token
+ uri = if controller_id
+ "/restapi/user-locate?cid=#{controller_id}&key=#{URI.encode_www_form key}"
+ else
+ "/restapi/user-locate?name=#{URI.encode_www_form key}"
+ end
+
+ response = get(uri, headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ })
+
+ if response.success?
+ resp = LocateResponse.from_json(response.body.not_nil!)
+ # Select users where there is a desk key found
+ check_response(resp).select(&.key)
+ else
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ protected def check_response(resp)
+ if resp.result
+ resp.info.not_nil!
+ else
+ raise "bad response result (#{resp.code}) #{resp.message}"
+ end
+ end
+end
diff --git a/drivers/floorsense/desks_spec.cr b/drivers/floorsense/desks_spec.cr
new file mode 100644
index 00000000000..c337a454df1
--- /dev/null
+++ b/drivers/floorsense/desks_spec.cr
@@ -0,0 +1,25 @@
+DriverSpecs.mock_driver "Floorsense::Desks" do
+ # Send the request
+ retval = exec(:get_token)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |request, response|
+ if io = request.body
+ data = io.gets_to_end
+
+ # The request is param encoded
+ if data == "username=srvc_acct&password=password%21"
+ response.status_code = 200
+ response.output.puts %({"type":"response","result":true,"message":"Authentication successful","info":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzbWFydGFsb2NrLWQ1MGJjZC5sb2NhbGRvbWFpbiIsInN1YiI6ImFjYSIsImF1ZCI6ImFwaSIsImV4cCI6MTU3MjMwODMzMiwiaWF0IjoxNTcyMzA0NzMyfQ.KMlzvjYPFw9e5d5LQjb1BF5R1Je9KkgoigkNOUZnR4U","sessionid":"ace555fe-4914-4203-b0a3-a1a6f532fef7"}})
+ else
+ response.status_code = 401
+ response.output.puts %({"type":"response","result":false,"message":"Authentication failed","code":17})
+ end
+ else
+ raise "expected request to include username and password"
+ end
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzbWFydGFsb2NrLWQ1MGJjZC5sb2NhbGRvbWFpbiIsInN1YiI6ImFjYSIsImF1ZCI6ImFwaSIsImV4cCI6MTU3MjMwODMzMiwiaWF0IjoxNTcyMzA0NzMyfQ.KMlzvjYPFw9e5d5LQjb1BF5R1Je9KkgoigkNOUZnR4U")
+end
diff --git a/drivers/floorsense/location_service.cr b/drivers/floorsense/location_service.cr
new file mode 100644
index 00000000000..a6645e6f295
--- /dev/null
+++ b/drivers/floorsense/location_service.cr
@@ -0,0 +1,130 @@
+module Floorsense; end
+
+require "uri"
+require "json"
+require "oauth2"
+require "placeos-driver/interface/locatable"
+require "./models"
+
+class Floorsense::LocationService < PlaceOS::Driver
+ include Interface::Locatable
+
+ descriptive_name "Floorsense Location Service"
+ generic_name :FloorsenseLocationService
+ description %(collects desk booking data from the staff API and overlays Floorsense data for visualising on a map)
+
+ accessor floorsense : Floorsense_1
+
+ default_settings({
+ floor_mappings: {
+ "planid": {
+ building_id: "zone-building",
+ level_id: "zone-level",
+ name: "friendly name for documentation",
+ },
+ },
+ include_bookings: false,
+ })
+
+ @floor_mappings : Hash(String, NamedTuple(building_id: String?, level_id: String)) = {} of String => NamedTuple(building_id: String?, level_id: String)
+ # Level zone => plan_id
+ @zone_mappings : Hash(String, String) = {} of String => String
+ # Level zone => building_zone
+ @building_mappings : Hash(String, String?) = {} of String => String?
+
+ @include_bookings : Bool = false
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @include_bookings = setting?(Bool, :include_bookings) || false
+ @floor_mappings = setting(Hash(String, NamedTuple(building_id: String?, level_id: String)), :floor_mappings)
+ @floor_mappings.each do |plan_id, details|
+ level = details[:level_id]
+ @building_mappings[level] = details[:building_id]
+ @zone_mappings[level] = plan_id
+ end
+ end
+
+ # ===================================
+ # Locatable Interface functions
+ # ===================================
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "sensor incapable of locating #{email} or #{username}" }
+ [] of Nil
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ logger.debug { "sensor incapable of tracking #{email} or #{username}" }
+ [] of String
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ floor_mac = URI::Params.parse mac_address
+ user = floorsense.at_location(floor_mac["cid"], floor_mac["key"]).get
+ {
+ location: "desk",
+ assigned_to: user["name"].as_s,
+ mac_address: mac_address,
+ }
+ rescue
+ nil
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching locatable in zone #{zone_id}" }
+ return [] of Nil if location && location != "desk"
+
+ plan_id = @zone_mappings[zone_id]?
+ return [] of Nil unless plan_id
+
+ building = @building_mappings[zone_id]?
+
+ raw_desks = floorsense.desks(plan_id).get.to_json
+ desks = Array(DeskStatus).from_json(raw_desks).compact_map do |desk|
+ if desk.occupied
+ {
+ location: :desk,
+ at_location: 1,
+ map_id: desk.key,
+ level: zone_id,
+ building: building,
+ capacity: 1,
+
+ # So we can look up who is at a desk at some point in the future
+ mac: "cid=#{desk.cid}&key=#{desk.key}",
+
+ floorsense_status: desk.status,
+ floorsense_desk_type: desk.desk_type,
+ }
+ end
+ end
+
+ current = [] of BookingStatus
+
+ if @include_bookings
+ raw_bookings = floorsense.bookings(plan_id).get.to_json
+ Hash(String, Array(BookingStatus)).from_json(raw_bookings).each_value do |bookings|
+ current << bookings.first unless bookings.empty?
+ end
+ end
+
+ current.map { |booking|
+ {
+ location: :booking,
+ type: "desk",
+ checked_in: booking.active,
+ asset_id: booking.key,
+ booking_id: booking.booking_id,
+ building: building,
+ level: zone_id,
+ ends_at: booking.finish,
+ mac: "cid=#{booking.cid}&key=#{booking.key}",
+ staff_email: booking.user.try &.email,
+ staff_name: booking.user.try &.name,
+ }
+ } + desks
+ end
+end
diff --git a/drivers/floorsense/location_service_spec.cr b/drivers/floorsense/location_service_spec.cr
new file mode 100644
index 00000000000..8c2d48c4ebf
--- /dev/null
+++ b/drivers/floorsense/location_service_spec.cr
@@ -0,0 +1,72 @@
+DriverSpecs.mock_driver "Floorsense::LocationService" do
+ system({
+ Floorsense: {Floorsense},
+ })
+
+ resp = exec(:device_locations, "zone-level").get
+ resp.should eq([
+ {"location" => "desk", "at_location" => 1, "map_id" => "D403-01", "level" => "zone-level", "building" => "zone-building", "capacity" => 1, "mac" => "cid=3&key=D403-01", "floorsense_status" => 17, "floorsense_desk_type" => "a"},
+ ])
+end
+
+class Floorsense < DriverSpecs::MockDriver
+ def desks(plan_id : String)
+ JSON.parse %([
+ {
+ "cid": 14,
+ "status": 17,
+ "cached": true,
+ "eui64": "00124b0018ae56d0",
+ "occupied": false,
+ "freq": "915",
+ "groupid": 0,
+ "netid": 3,
+ "key": "915-09",
+ "reservable": true,
+ "bkid": "",
+ "deskid": 2,
+ "hwfeat": 0,
+ "created": 1568887923,
+ "hardware": "E-20",
+ "firmware": "401",
+ "type": "a",
+ "planid": 6,
+ "reserved": false,
+ "features": 0,
+ "confirmed": false,
+ "privacy": false,
+ "uid": "",
+ "occupiedtime": 0
+ },
+ {
+ "cid": 3,
+ "status": 17,
+ "cached": true,
+ "eui64": "00124b0018ae54e5",
+ "occupied": true,
+ "freq": "",
+ "groupid": 0,
+ "netid": 2,
+ "key": "D403-01",
+ "reservable": false,
+ "bkid": "",
+ "deskid": 129,
+ "hwfeat": 0,
+ "created": 1568887941,
+ "hardware": "",
+ "firmware": "",
+ "type": "a",
+ "planid": 6,
+ "reserved": false,
+ "features": 0,
+ "confirmed": false,
+ "privacy": false,
+ "uid": "",
+ "occupiedtime": 0
+ }])
+ end
+
+ def bookings(plan_id : String)
+ JSON.parse %({})
+ end
+end
diff --git a/drivers/floorsense/models.cr b/drivers/floorsense/models.cr
new file mode 100644
index 00000000000..0c4d76157b9
--- /dev/null
+++ b/drivers/floorsense/models.cr
@@ -0,0 +1,329 @@
+require "json"
+
+# Floorsense Data Models
+module Floorsense
+ class AuthResponse
+ include JSON::Serializable
+
+ class Info
+ include JSON::Serializable
+
+ property token : String
+ property sessionid : String
+ end
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+ property message : String?
+
+ # Returned on failure
+ property code : Int32?
+
+ # Returned on success
+ property info : Info?
+ end
+
+ class DeskStatus
+ include JSON::Serializable
+
+ property cid : Int32
+ property cached : Bool
+ property reservable : Bool
+ property netid : Int32
+ property status : Int32
+ property deskid : Int32
+
+ property hwfeat : Int32
+ property hardware : String
+
+ @[JSON::Field(converter: Time::EpochConverter)]
+ property created : Time
+ property key : String
+ property occupied : Bool
+ property uid : String
+ property eui64 : String
+
+ @[JSON::Field(key: "type")]
+ property desk_type : String
+ property firmware : String
+ property features : Int32
+ property freq : String
+ property groupid : Int32
+ property bkid : String
+ property planid : Int32
+ property reserved : Bool
+ property confirmed : Bool
+ property privacy : Bool
+ property occupiedtime : Int32
+ end
+
+ class DesksResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : Array(DeskStatus)?
+ end
+
+ class UserLocation
+ include JSON::Serializable
+
+ property name : String
+ property uid : String
+
+ # Optional properties (when a user is located):
+
+ @[JSON::Field(converter: Time::EpochConverter)]
+ property start : Time?
+
+ @[JSON::Field(converter: Time::EpochConverter)]
+ property finish : Time?
+
+ property planid : Int32?
+ property occupied : Bool?
+ property groupid : Int32?
+ property key : String?
+ property floorname : String?
+ property cid : Int32?
+ property occupiedtime : Int32?
+ property groupname : String?
+ property privacy : Bool?
+ property confirmed : Bool?
+ property active : Bool?
+ end
+
+ class LocateResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : Array(UserLocation)?
+ end
+
+ class Floor
+ include JSON::Serializable
+
+ property planid : Int32
+ property name : String
+
+ property imgname : String?
+ property imgwidth : Int32?
+ property imgheight : Int32?
+
+ property location1 : String?
+ property location2 : String?
+ property location3 : String?
+ end
+
+ class FloorsResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : Array(Floor)?
+ end
+
+ class BookingStatus
+ include JSON::Serializable
+
+ property key : String
+ property uid : String
+
+ @[JSON::Field(key: "bktype")]
+ property booking_type : String
+
+ @[JSON::Field(key: "bkid")]
+ property booking_id : String
+
+ property desc : String?
+ property created : Int64
+ property start : Int64
+ property finish : Int64
+
+ property conftime : Int64?
+ property confmethod : Int32?
+ property confexpiry : Int64?
+
+ property cid : Int32
+ property planid : Int32
+ property groupid : Int32
+
+ # Time the booking was released
+ property released : Int64
+ property releasecode : Int32
+ property active : Bool
+ property confirmed : Bool
+ property privacy : Bool
+
+ # not included in the responses but we will merge this
+ property user : User?
+ end
+
+ class BookingsResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success (desk => bookings)
+ property info : Hash(String, Array(BookingStatus))?
+ end
+
+ class BookingResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+ property info : BookingStatus?
+ end
+
+ class User
+ include JSON::Serializable
+
+ property uid : String
+ property email : String?
+ property name : String
+ property desc : String?
+ property lastlogin : Int64?
+ property expiry : Int64?
+ end
+
+ class UserResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : User?
+ end
+
+ class UsersResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : Array(User)?
+ end
+
+ class LogEntry
+ include JSON::Serializable
+
+ property eventid : Int64
+
+ # this is the locker or table name
+ property key : String
+
+ # the event code
+ property code : Int32
+
+ # booking id
+ property bkid : String
+
+ # Possibly includes the booking information
+ # not required as we need to grab the user information anyway
+ # property extra : JSON::Any?
+
+ property eventtime : Int64
+ end
+
+ class LogResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : Array(LogEntry)?
+ end
+
+ class RFID
+ include JSON::Serializable
+
+ property csn : String
+ property uid : String
+ property desc : String?
+ end
+
+ class RFIDResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ # Returned on success
+ property info : RFID?
+ end
+
+ class GenericResponse
+ include JSON::Serializable
+
+ @[JSON::Field(key: "type")]
+ property msg_type : String
+ property result : Bool
+
+ # Returned on failure
+ property message : String?
+ property code : Int32?
+
+ property info : JSON::Any?
+
+ def info
+ @info || JSON::Any.new(true)
+ end
+ end
+end
diff --git a/drivers/freespace/models.cr b/drivers/freespace/models.cr
new file mode 100644
index 00000000000..44f6b495f25
--- /dev/null
+++ b/drivers/freespace/models.cr
@@ -0,0 +1,126 @@
+require "json"
+
+module Freespace
+ class SpaceActivity
+ include JSON::Serializable
+
+ property id : Int64
+
+ @[JSON::Field(key: "spaceId")]
+ property space_id : Int64
+
+ @[JSON::Field(key: "utcEpoch")]
+ property utc_epoch : Int64
+ property state : Int32
+
+ def presence?
+ @state > 0
+ end
+
+ @[JSON::Field(ignore: true)]
+ property! location_id : Int64
+
+ @[JSON::Field(ignore: true)]
+ property! capacity : Int32
+
+ @[JSON::Field(ignore: true)]
+ property! name : String
+ end
+
+ # ====
+ # Classes related to a space
+ # ====
+
+ class Location
+ include JSON::Serializable
+
+ property id : Int64
+
+ # undocumented, can be nil
+ # @[JSON::Field(key: "scalingFactor")]
+ # property scaling_factor : Float64?
+
+ property raw : Bool
+ property policy : Bool
+ end
+
+ class SRF
+ include JSON::Serializable
+
+ property x : Int32
+ property y : Int32
+ property z : Int32
+ end
+
+ class Category
+ include JSON::Serializable
+
+ property id : Int64
+ property name : String
+
+ @[JSON::Field(key: "shortName")]
+ property short_name : String?
+
+ @[JSON::Field(key: "showOnSignage")]
+ property show_on_signage : Bool
+
+ @[JSON::Field(key: "showInAnalytics")]
+ property show_in_analytics : Bool
+
+ @[JSON::Field(key: "iconUrl")]
+ property icon_url : String?
+
+ # RGB value i.e. #ffb3b3
+ @[JSON::Field(key: "colorScheme")]
+ property color_scheme : String?
+
+ @[JSON::Field(key: "orderingIndex")]
+ property ordering_index : Int32?
+ end
+
+ class Device
+ include JSON::Serializable
+
+ property id : Int64
+
+ @[JSON::Field(key: "displayName")]
+ property name : String
+
+ # Many more undocumented fields
+ end
+
+ class Space
+ include JSON::Serializable
+
+ property id : Int64
+ property location : Location
+ property name : String
+ property srf : SRF
+
+ # undocumented, possibly polymorphic: {"type" => "CIRCLE", "data" => "20"},
+ property marker : Hash(String, JSON::Any)
+
+ @[JSON::Field(key: "subCategory")]
+ property sub_category : Category
+ property category : Category
+ property department : Category
+
+ @[JSON::Field(key: "sensingPolicyId")]
+ property sensing_policy_id : Int32
+ property device : Device
+
+ @[JSON::Field(key: "markerUniqueId")]
+ property marker_unique_id : String?
+ property live : Bool
+ property capacity : Int32
+
+ # unsure about this field
+ # property counter : String
+
+ property serial : Int32
+
+ @[JSON::Field(key: "locationId")]
+ property location_id : Int64
+ property counted : Bool
+ end
+end
diff --git a/drivers/freespace/sensor_api.cr b/drivers/freespace/sensor_api.cr
new file mode 100644
index 00000000000..608743df3f9
--- /dev/null
+++ b/drivers/freespace/sensor_api.cr
@@ -0,0 +1,243 @@
+module Freespace; end
+
+require "placeos-driver/interface/locatable"
+require "uri"
+require "stomp"
+require "./models"
+
+# https://aca.im/driver_docs/Freespace/Freespace%20Socket%20API-V1.2.pdf
+
+class Freespace::SensorAPI < PlaceOS::Driver
+ include Interface::Locatable
+
+ # Discovery Information
+ generic_name :Freespace
+ descriptive_name "Freespace Websocket API"
+
+ uri_base "https://_instance_.afreespace.com"
+
+ default_settings({
+ username: "user",
+ password: "pass",
+
+ floor_mappings: {
+ "775" => {
+ building_id: "zone-building",
+ level_id: "zone-level",
+ name: "friendly name for documentation",
+ },
+ },
+ })
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @username = setting(String, :username)
+ @password = setting(String, :password)
+ @floor_mappings = setting(Hash(String, NamedTuple(building_id: String?, level_id: String)), :floor_mappings)
+
+ # configure the zone mappings
+ @zone_mappings.clear
+ @floor_mappings.each do |location_id, details|
+ @zone_mappings[details[:level_id]] << location_id
+ @zone_mappings[details[:building_id]] << location_id
+ end
+
+ # We want to rebind to everything
+ disconnect if @connected
+ end
+
+ # We need an API key to connect to the websocket
+ def websocket_headers
+ HTTP::Headers{
+ "X-Auth-Key" => get_token,
+ }
+ end
+
+ getter! client : STOMP::Client
+ @auth_key : String? = nil
+ @spaces : Hash(Int64, Space) = {} of Int64 => Space
+ @space_state : Hash(Int64, SpaceActivity) = {} of Int64 => SpaceActivity
+ @username : String = ""
+ @password : String = ""
+ @connected : Bool = false
+
+ @floor_mappings : Hash(String, NamedTuple(building_id: String?, level_id: String)) = {} of String => NamedTuple(building_id: String?, level_id: String)
+ # Level zone => location_id
+ @zone_mappings : Hash(String, Array(String)) = Hash(String, Array(String)).new { |hash, key| hash[key] = [] of String }
+
+ def connected
+ @connected = true
+
+ # Send the CONNECT message
+ hostname = URI.parse(config.uri.not_nil!).hostname.not_nil!
+ @client = STOMP::Client.new(hostname)
+ send(client.stomp.to_s)
+
+ schedule.clear
+ schedule.in(5.seconds) { @auth_key = nil }
+ schedule.every(10.seconds) { heart_beat }
+ end
+
+ def disconnected
+ @connected = false
+ schedule.clear
+ @spaces.clear
+ @auth_key = @client = nil
+ end
+
+ def heart_beat
+ send(client.send("/beat/#{Time.utc.to_unix}").to_s, wait: false, priority: 0)
+ end
+
+ protected def subscribe_location(location_id) : Nil
+ get_location(location_id).each do |space|
+ id = space.id
+ request = client.subscribe("space-#{id}", "/topic/spaces/#{id}/activities", HTTP::Headers{
+ "receipt" => "rec-#{id}",
+ })
+
+ # Wait false as the server is not STOMP compliant, it won't respond to receipt headers
+ send(request.to_s, wait: false)
+ end
+ end
+
+ @[Security(Level::Support)]
+ def spaces_details
+ @spaces
+ end
+
+ @[Security(Level::Support)]
+ def spaces_state
+ @space_state
+ end
+
+ @[Security(Level::Support)]
+ def get_location(location_id : String | Int64) : Array(Space)
+ response = http("POST",
+ "/api/locations/#{location_id}/spaces",
+ headers: {
+ "X-Auth-Key" => get_token,
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ }, body: {
+ username: @username,
+ password: @password,
+ }.to_json
+ )
+
+ raise "issue obtaining to location #{location_id}: status code #{response.status_code}\n#{response.body}" unless response.success?
+
+ spaces = Array(Space).from_json response.body
+ spaces.each { |space| @spaces[space.id] = space }
+ spaces
+ end
+
+ # Alternative to using basic auth, but here really only for testing with postman
+ @[Security(Level::Support)]
+ def get_token : String
+ auth_key = @auth_key
+ return auth_key if auth_key
+
+ response = http("POST",
+ "/login",
+ headers: {
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ }, body: {
+ username: @username,
+ password: @password,
+ }.to_json
+ )
+ logger.debug { "login response: #{response.body}" }
+ raise "issue obtaining token: #{response.status_code}\n#{response.body}" unless response.success?
+
+ # auth key is valid for 5 seconds
+ schedule.in(5.seconds) { @auth_key = nil }
+ @auth_key = response.headers["X-Auth-Key"]
+ end
+
+ def received(bytes, task)
+ frame = STOMP::Frame.new(bytes)
+
+ case frame.command
+ when .connected?
+ client.negotiate(frame)
+ @floor_mappings.keys.each do |location_id|
+ begin
+ subscribe_location(location_id)
+ rescue error
+ logger.error(exception: error) { "failed to subscribe to #{location_id}, skipping" }
+ end
+ end
+ when .message?
+ activity = SpaceActivity.from_json(frame.body_text)
+ if space = @spaces[activity.space_id]?
+ activity.location_id = space.location_id
+ activity.capacity = space.capacity
+ activity.name = space.name
+ @space_state[activity.space_id] = activity
+ self["space-#{activity.space_id}"] = {
+ location: space.location_id,
+ name: space.name,
+ capacity: space.capacity,
+ count: activity.state,
+ last_updated: activity.utc_epoch,
+ }
+ self["last_change"] = Time.utc.to_unix
+ else
+ # NOTE:: this should never happen
+ logger.warn { "unknown space id: #{activity.space_id}" }
+ end
+ end
+
+ task.try &.success
+ end
+
+ # ===================================
+ # Locatable Interface functions
+ # ===================================
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "sensor incapable of locating #{email} or #{username}" }
+ [] of Nil
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ logger.debug { "sensor incapable of tracking #{email} or #{username}" }
+ [] of String
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ logger.debug { "sensor incapable of tracking #{mac_address}" }
+ nil
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching locatable in zone #{zone_id}" }
+ return [] of Nil if location && location != "desk"
+
+ loctions = @zone_mappings[zone_id]?
+ return [] of Nil unless loctions
+
+ # loc_id is a string
+ loctions.flat_map do |loc_id|
+ location_id = loc_id.to_i64
+ loc_details = @floor_mappings[loc_id]
+
+ @space_state.values.compact_map do |activity|
+ next if activity.location_id != location_id || activity.state == 0 || activity.capacity > 1
+
+ {
+ location: activity.capacity == 1 ? "desk" : "area",
+ at_location: activity.state,
+ map_id: activity.name,
+ level: loc_details[:level_id],
+ building: loc_details[:building_id],
+ capacity: activity.capacity,
+ }
+ end
+ end
+ end
+end
diff --git a/drivers/freespace/sensor_api_spec.cr b/drivers/freespace/sensor_api_spec.cr
new file mode 100644
index 00000000000..0ff1bd206a4
--- /dev/null
+++ b/drivers/freespace/sensor_api_spec.cr
@@ -0,0 +1,153 @@
+require "json"
+require "stomp"
+
+DriverSpecs.mock_driver "Freespace::SensorAPI" do
+ # ===========
+ # Negotiation
+ # ===========
+
+ client = STOMP::Client.new("127.0.0.1")
+ should_send(client.stomp.to_s)
+
+ connect_message = STOMP::Frame.new(STOMP::Command::Connected, HTTP::Headers{
+ "version" => "1.2",
+ # server sends a blank heartbeat
+ "heart-beat" => "0,0",
+ })
+ responds(connect_message.to_s)
+
+ # ==========
+ # GET SPACES
+ # ==========
+
+ expect_http_request do |request, response|
+ headers = request.headers
+ io = request.body
+ if io = request.body
+ data = io.gets_to_end
+ request = JSON.parse(data)
+ if request["username"] == "user" && request["password"] == "pass"
+ response.status_code = 200
+ response.headers["X-Auth-Key"] = "12345"
+ else
+ response.status_code = 401
+ end
+ else
+ raise "expected request to include dialing details #{request.inspect}"
+ end
+ end
+
+ expect_http_request do |request, response|
+ headers = request.headers
+
+ if headers["X-Auth-Key"]? == "12345"
+ response.status_code = 200
+ response.output.puts SPACES_RESPONSE
+ else
+ response.status_code = 401
+ end
+ end
+
+ # =============
+ # Subscriptions
+ # =============
+
+ should_send(client.subscribe("space-96978", "/topic/spaces/96978/activities", HTTP::Headers{
+ "receipt" => "rec-96978",
+ }).to_s)
+
+ # ==========
+ # GET TOKEN
+ # ==========
+
+ # cached
+ retval = exec(:get_token)
+ retval.get.should eq("12345")
+
+ # =============
+ # Status update
+ # =============
+ time_now = Time.utc.to_unix
+ status_update = STOMP::Frame.new(STOMP::Command::Message, HTTP::Headers{
+ "subscription" => "space-96978",
+ "destination" => "/topic/spaces/96978/activities",
+ }, {
+ id: 1234,
+ spaceId: 96978,
+ utcEpoch: time_now,
+ state: 1,
+ }.to_json)
+
+ transmit status_update.to_s
+
+ status["space-96978"].should eq({
+ "location" => 775,
+ "name" => "WS7-01",
+ "capacity" => 1,
+ "count" => 1,
+ "last_updated" => time_now,
+ })
+
+ # =================
+ # location services
+ # =================
+ exec(:device_locations, "zone-building").get.should eq([{
+ "location" => "desk",
+ "at_location" => 1,
+ "map_id" => "WS7-01",
+ "level" => "zone-level",
+ "building" => "zone-building",
+ "capacity" => 1,
+ }])
+end
+
+SPACES_RESPONSE = [{"id" => 96978,
+ "location" => {"id" => 775, "scalingFactor" => nil, "raw" => true, "policy" => true},
+ "name" => "WS7-01",
+ "srf" => {"x" => 91, "y" => 2169, "z" => 0},
+ "marker" => {"type" => "CIRCLE", "data" => "20"},
+ "category" => {"id" => 297,
+ "name" => "Assigned Desks",
+ "shortName" => nil,
+ "showOnSignage" => false,
+ "showInAnalytics" => true,
+ "iconUrl" => nil,
+ "colorScheme" => "#ffb3b3",
+ "orderingIndex" => 113},
+ "sensingPolicyId" => 247,
+ "department" => {"id" => 498,
+ "name" => "Sales",
+ "shortName" => nil,
+ "showOnSignage" => false,
+ "showInAnalytics" => false,
+ "colorScheme" => nil,
+ "orderingIndex" => nil},
+ "subCategory" => {"id" => 194,
+ "name" => "None",
+ "shortName" => nil,
+ "showOnSignage" => false,
+ "showInAnalytics" => false,
+ "colorScheme" => nil,
+ "orderingIndex" => 194},
+ "device" => {"id" => 2016090160,
+ "displayName" => "1609010160",
+ "updatedAt" => nil,
+ "floorId" => nil,
+ "shape" => nil,
+ "coord" => nil,
+ "blessId" => 1609010160,
+ "blessQr" => nil,
+ "accessedAt" => "2021-03-11T08:06:01.000+0000",
+ "installedOn" => nil,
+ "licenseeId" => nil,
+ "hardware" => nil,
+ "network" => nil,
+ "itemId" => nil},
+ "markerUniqueId" => "K_2493713878097_18542",
+ "live" => false,
+ "capacity" => 1,
+ "counter" => "NO_COUNTER",
+ "serial" => 1,
+ "locationId" => 775,
+ "counted" => true,
+}].to_json
diff --git a/drivers/gantner/relaxx/json_models.cr b/drivers/gantner/relaxx/json_models.cr
new file mode 100644
index 00000000000..c9e5fa66712
--- /dev/null
+++ b/drivers/gantner/relaxx/json_models.cr
@@ -0,0 +1,137 @@
+require "json"
+
+module Gantner; end
+
+module Gantner::Relaxx
+ class Result
+ include JSON::Serializable
+
+ @[JSON::Field(key: "Successful")]
+ property successful : Bool
+
+ @[JSON::Field(key: "Cancelled")]
+ property cancelled : Bool
+
+ @[JSON::Field(key: "ResultText")]
+ property text : String
+
+ @[JSON::Field(key: "ResultCode")]
+ property code : Int32
+ end
+
+ enum LockerState
+ Unknown = 0
+ Disabled
+ Free
+ InUse
+ Locked
+ Alarmed
+ InUseExpired
+ Conflict
+ end
+
+ enum LockerMode
+ Unknown = 0
+ NotExisting
+ FreeLocker
+ PersonalLocker
+ ReservableLocker
+ DynamicLocker
+ end
+
+ class Locker
+ include JSON::Serializable
+
+ @[JSON::Field(key: "RecordId")]
+ property id : String
+
+ @[JSON::Field(key: "LockerGroupId")]
+ property group_id : String
+
+ @[JSON::Field(key: "LockerGroupName")]
+ property group_name : String
+
+ @[JSON::Field(key: "Number")]
+ property locker_number : String
+
+ @[JSON::Field(key: "Address")]
+ property address : Int32
+
+ @[JSON::Field(key: "State")]
+ property state : Int32
+
+ @[JSON::Field(key: "LockerMode")]
+ property mode : Int32
+
+ # Is it a personal locker or a free (no cost?) locker?
+ @[JSON::Field(key: "IsFreeLocker")]
+ property is_free : Bool
+
+ @[JSON::Field(key: "IsDeleted")]
+ property is_deleted : Bool
+
+ @[JSON::Field(key: "IsExisting")]
+ property is_existing : Bool
+
+ @[JSON::Field(key: "LastClosedTime")]
+ property last_closed : String
+
+ @[JSON::Field(key: "CardUIDInUse")]
+ property card_id : String
+
+ def locker_state
+ LockerState.from_value self.state
+ end
+
+ def locker_mode
+ LockerMode.from_value self.mode
+ end
+ end
+
+ enum LockerEvent
+ Opened = 0
+ Closed
+ Enabled
+ Disabled
+ Alarmed
+ end
+
+ class LockerNotification
+ include JSON::Serializable
+
+ @[JSON::Field(key: "Event")]
+ property event : Int32
+
+ @[JSON::Field(key: "PreviousState")]
+ property prev_state : Int32
+
+ @[JSON::Field(key: "EventDateTime")]
+ property time : String
+
+ @[JSON::Field(key: "Locker")]
+ property locker : Locker
+
+ @[JSON::Field(key: "LockerAreaId")]
+ property area_id : String
+
+ @[JSON::Field(key: "LockerAreaName")]
+ property area_name : String
+
+ @[JSON::Field(key: "WithMasterCard")]
+ property group_name : Bool
+
+ @[JSON::Field(key: "WithSystemCard")]
+ property group_name : Bool
+
+ @[JSON::Field(key: "WithMaintenanceCard")]
+ property group_name : Bool
+
+ def locker_state
+ self.locker.state
+ end
+
+ def previous_state
+ LockerState.from_value self.prev_state
+ end
+ end
+end
diff --git a/drivers/gantner/relaxx/protocol_json.cr b/drivers/gantner/relaxx/protocol_json.cr
new file mode 100644
index 00000000000..f7c38b38c62
--- /dev/null
+++ b/drivers/gantner/relaxx/protocol_json.cr
@@ -0,0 +1,237 @@
+module Gantner; end
+
+require "openssl/cipher"
+require "./json_models"
+require "base64"
+require "uuid"
+require "set"
+
+# Documentation: https://aca.im/driver_docs/gantner/GAT-Relaxx-JSON-Interface-Description-2.10.pdf
+# REST Docs: https://doc.gantner.com/RelaxxDocs/GatRelaxxRestAPI.yaml
+# https://aca.im/driver_docs/gantner/GatRelaxxRestAPI.yaml
+# PC Application
+# User = Administrator
+# PW = Mirone59
+
+class Gantner::Relaxx::ProtocolJSON < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 8237
+ descriptive_name "Gantner GAT Relaxx JSON API"
+ generic_name :Lockers
+
+ @authenticated : Bool = false
+ @password : String = "GAT"
+
+ # Lists of locker IDs
+ @locker_ids : Set(String) = Set(String).new
+ @lockers_in_use : Set(String) = Set(String).new
+
+ def on_load
+ # 0x02 (Start of frame) and 0x03 (End of frame)
+ transport.tokenizer = Tokenizer.new(Bytes[0x03])
+ on_update
+ end
+
+ def on_update
+ @password = setting?(String, :password) || "GAT"
+ end
+
+ # Converts the data to bytes and wraps it into a frame
+ private def send_frame(data, **options)
+ logger.debug { "requesting #{data[:Caption]}, id #{data[:Id]}" }
+ send "\x02#{data.to_json}\x03", **options
+ end
+
+ private def new_request_id
+ UUID.random.to_s.upcase
+ end
+
+ def connected
+ self["authenticated"] = @authenticated = false
+ request_auth_string
+
+ schedule.every(40.seconds) do
+ logger.debug { "-- maintaining connection" }
+ @authenticated ? keep_alive : request_auth_string
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def keep_alive
+ send_frame({
+ Caption: "KeepAliveRequest",
+ Id: new_request_id,
+ }, priority: 0)
+ end
+
+ def request_auth_string
+ send_frame({
+ Caption: "AuthenticationRequestA",
+ Id: new_request_id,
+ }, priority: 9998)
+ end
+
+ private def login(authentication_string : String)
+ cipher = OpenSSL::Cipher.new("aes-256-cbc")
+ cipher.padding = true
+ cipher.decrypt
+
+ # LE for little endian and avoids a byte order mark
+ password = @password.encode("UTF-16LE")
+
+ key = IO::Memory.new(Bytes.new(32))
+ key.write password
+
+ iv = IO::Memory.new(Bytes.new(16))
+ iv.write password
+
+ cipher.key = key.to_slice
+ cipher.iv = iv.to_slice
+
+ decrypted_data = IO::Memory.new
+ content = Base64.decode(authentication_string)
+ decrypted_data.write cipher.update(content)
+ decrypted_data.write cipher.final
+ decrypted_data.rewind
+
+ # Return the decrypted string
+ decrypted = String.new(decrypted_data.to_slice, "UTF-16LE")
+
+ send_frame({
+ Caption: "AuthenticationRequestB",
+ Id: new_request_id,
+
+ # Locker system expects an integer here
+ AuthenticationString: decrypted.to_i,
+ }, priority: 9999)
+ end
+
+ def open_locker(locker_number : String, locker_group : String? = nil)
+ set_open_state(true, locker_number, locker_group)
+ end
+
+ def close_locker(locker_number : String, locker_group : String? = nil)
+ set_open_state(false, locker_number, locker_group)
+ end
+
+ def set_open_state(open : Bool, locker_number : String, locker_group : String? = nil)
+ action = open ? "0" : "1"
+
+ # Detect if this is a GUID
+ task = if locker_number.includes?("-")
+ send_frame({
+ Caption: "ExecuteLockerActionRequest",
+ Id: new_request_id,
+ Action: action,
+ LockerId: locker_number,
+ })
+ else
+ request = {
+ Caption: "ExecuteLockerActionRequest",
+ Id: new_request_id,
+ Action: action,
+ LockerNumber: locker_number,
+ }
+ if locker_group
+ send_frame(request.merge({LockerGroupId: locker_group}))
+ else
+ send_frame(request)
+ end
+ end
+
+ task
+ end
+
+ def query_lockers(free_only : Bool = false)
+ send_frame({
+ Caption: "GetLockersRequest",
+ Id: new_request_id,
+ FreeLockersOnly: free_only,
+ PersonalLockersOnly: false,
+ })
+ end
+
+ def received(data, task)
+ # Ignore the framing bytes
+ data = String.new(data)[1..-2]
+ logger.debug { "Gantner Relaxx sent: #{data}" }
+ json = JSON.parse(data)
+
+ # Ignore if a notification as we still might be expecting a response
+ return parse_notify(json["Caption"].as_s, data) if json["IsNotification"].as_bool
+
+ # Check result of the request
+ result = Result.from_json(json["Result"].to_json)
+ if result.cancelled
+ return task.try &.abort("request cancelled, #{result.code}: #{result.text}")
+ end
+ if !result.successful
+ return task.try &.abort("request failed, #{result.code}: #{result.text}")
+ end
+
+ # Process response
+ case json["Caption"].as_s
+ when "AuthenticationResponseA"
+ logged_in = json["LoggedIn"].as_bool
+ self["authenticated"] = @authenticated = logged_in
+ return task.try &.success if logged_in
+ login(json["AuthenticationString"].as_s)
+ when "AuthenticationResponseB"
+ logged_in = json["LoggedIn"].as_bool
+ self["authenticated"] = @authenticated = logged_in
+ if logged_in
+ logger.debug { "authentication success" }
+
+ # Obtain the list of lockers and their current state
+ query_lockers if @locker_ids.empty?
+ else
+ logger.warn { "authentication failure - please check credentials" }
+ end
+ when "GetLockersResponse"
+ lockers = Array(Locker).from_json(json["Lockers"].to_json)
+ lockers.each do |locker|
+ locker_id = locker.id
+ @locker_ids << locker_id
+ if locker.locker_state != LockerState::Free
+ @lockers_in_use << locker_id
+ self["locker_#{locker_id}"] = locker.card_id
+ else
+ @lockers_in_use.delete(locker_id)
+ end
+ end
+ self[:locker_ids] = @locker_ids
+ self[:lockers_in_use] = @lockers_in_use
+ when "CommandNotSupportedResponse"
+ logger.warn { "Command not supported!" }
+ return task.try &.abort("Command not supported!")
+ end
+
+ task.try &.success
+ end
+
+ private def parse_notify(caption, json)
+ case caption
+ when "LockerEventNotification"
+ info = LockerNotification.from_json(json)
+ update_locker_state(info.locker_state != LockerState::Free, info.locker.id, info.locker.card_id)
+ else
+ logger.debug { "ignoring event: #{caption}" }
+ end
+ nil
+ end
+
+ private def update_locker_state(in_use : Bool, locker_id : String, card_id : String) : Nil
+ @locker_ids << locker_id
+ if in_use
+ @lockers_in_use << locker_id
+ else
+ @lockers_in_use.delete(locker_id)
+ end
+ self["locker_#{locker_id}"] = card_id
+ self[:locker_ids] = @locker_ids
+ self[:lockers_in_use] = @lockers_in_use
+ end
+end
diff --git a/drivers/gantner/relaxx/protocol_json_spec.cr b/drivers/gantner/relaxx/protocol_json_spec.cr
new file mode 100644
index 00000000000..4e76ca467f3
--- /dev/null
+++ b/drivers/gantner/relaxx/protocol_json_spec.cr
@@ -0,0 +1,131 @@
+require "json"
+require "uuid"
+
+module Relaxx
+ SUCCESS = {
+ Successful: true,
+ Cancelled: false,
+ ResultText: "",
+ ResultCode: 0,
+ }
+
+ def self.frame(data)
+ "\x02#{data.to_json}\x03"
+ end
+
+ def self.parse(raw_data)
+ JSON.parse(String.new(raw_data)[1..-2])
+ end
+end
+
+DriverSpecs.mock_driver "Gantner::Relaxx::ProtocolJSON" do
+ # Should send an auth A request
+ data = Relaxx.parse(expect_send)
+ data["Caption"].as_s.should eq("AuthenticationRequestA")
+ id = data["Id"].as_s
+
+ # Respond with auth A response
+ transmit Relaxx.frame({
+ Caption: "AuthenticationResponseA",
+ Result: Relaxx::SUCCESS,
+ Id: UUID.random.to_s.upcase,
+ RequestId: id,
+ AuthenticationString: "wglgJg4kP8DHO+2+N6L8Hsu6mp3LSoe3/gIxDlZgu60=",
+ LoggedIn: false,
+ IsLoginCommand: true,
+ IsNotification: false,
+ IsResponse: true,
+ CustomTimeout: 0,
+ CompressContent: false,
+ })
+
+ # Should send an auth B request
+ data = Relaxx.parse(expect_send)
+ data["Caption"].as_s.should eq("AuthenticationRequestB")
+ id = data["Id"].as_s
+
+ # password should be decrypted
+ data["AuthenticationString"].as_i64.should eq(499520882)
+
+ # Respond with auth B response
+ transmit Relaxx.frame({
+ Caption: "AuthenticationResponseB",
+ Result: Relaxx::SUCCESS,
+ Id: UUID.random.to_s.upcase,
+ RequestId: id,
+ LoggedIn: true,
+ IsLoginCommand: true,
+ IsNotification: false,
+ IsResponse: true,
+ CustomTimeout: 0,
+ CompressContent: false,
+ })
+
+ # Expect a locker state query
+ data = Relaxx.parse(expect_send)
+ data["Caption"].as_s.should eq("GetLockersRequest")
+ id = data["Id"].as_s
+
+ locker_id1 = UUID.random.to_s
+ locker_id2 = UUID.random.to_s
+
+ transmit Relaxx.frame({
+ Caption: "GetLockersResponse",
+ Result: Relaxx::SUCCESS,
+ Id: UUID.random.to_s.upcase,
+ RequestId: id,
+ IsNotification: false,
+ IsResponse: true,
+ Lockers: [{
+ RecordId: locker_id1,
+ LockerGroupId: UUID.random.to_s,
+ LockerGroupName: "Example Group",
+ Number: "1",
+ Address: 21,
+ State: 2,
+ LockerMode: 3,
+ IsFreeLocker: false,
+ IsDeleted: false,
+ IsExisting: true,
+ LastClosedTime: "",
+ CardUIDInUse: "",
+ }, {
+ RecordId: locker_id2,
+ LockerGroupId: UUID.random.to_s,
+ LockerGroupName: "Example Group",
+ Number: "2",
+ Address: 21,
+ State: 3,
+ LockerMode: 3,
+ IsFreeLocker: false,
+ IsDeleted: false,
+ IsExisting: true,
+ LastClosedTime: "",
+ CardUIDInUse: "12345",
+ }],
+ })
+
+ # send a keep alive request
+ exec(:keep_alive)
+ data = Relaxx.parse(expect_send)
+ data["Caption"].as_s.should eq("KeepAliveRequest")
+ id = data["Id"].as_s
+
+ transmit Relaxx.frame({
+ Caption: "KeepAliveResponse",
+ Result: Relaxx::SUCCESS,
+ Id: UUID.random.to_s.upcase,
+ RequestId: id,
+ LoggedIn: true,
+ IsLoginCommand: false,
+ IsNotification: false,
+ IsResponse: true,
+ CustomTimeout: 0,
+ CompressContent: false,
+ })
+
+ status[:authenticated].should eq(true)
+ status[:locker_ids].should eq([locker_id1, locker_id2])
+ status[:lockers_in_use].should eq([locker_id2])
+ status["locker_#{locker_id2}"].should eq("12345")
+end
diff --git a/drivers/global_cache/gc_100.cr b/drivers/global_cache/gc_100.cr
new file mode 100644
index 00000000000..dfec93bbc86
--- /dev/null
+++ b/drivers/global_cache/gc_100.cr
@@ -0,0 +1,176 @@
+require "placeos-driver/interface/electrical_relay"
+
+class GlobalCache::Gc100 < PlaceOS::Driver
+ include Interface::ElectricalRelay
+
+ # Discovery Information
+ tcp_port 4999
+ descriptive_name "GlobalCache IO Gateway"
+ generic_name :DigitalIO
+
+ DELIMITER = "\r"
+
+ # @relay_config maps the GC100 into a linear set of ir and relays so models can be swapped in and out
+ # E.g. @relay_config = {"relay" => {0 => "2:1",1 => "2:2",2 => "2:3",3 => "3:1"}}
+ @relay_config : Hash(String, Hash(Int32, String)) = {} of String => Hash(Int32, String)
+ @port_config : Hash(String, Tuple(String, Int32)) = {} of String => Tuple(String, Int32)
+
+ def on_load
+ transport.tokenizer = Tokenizer.new(DELIMITER)
+ self[:num_relays] = 0
+ self[:num_ir] = 0
+ end
+
+ def connected
+ @relay_config = {} of String => Hash(Int32, String)
+ @port_config = {} of String => Tuple(String, Int32)
+ self[:config_indexed] = false
+
+ schedule.clear
+ schedule.every(10.seconds, true) do
+ logger.debug { "-- Polling GC100" }
+ get_devices unless self[:config_indexed].as_bool
+
+ # Low priority sent to maintain the connection
+ do_send("get_NET,0:1", priority: 0)
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def get_devices
+ do_send("getdevices") # , :max_waits => 100)
+ end
+
+ def relay(state : Bool, index : Int32 = 0, **options)
+ if index < self[:num_relays].as_i
+ relays = (self[:relay_config]["relay"]? || self[:relay_config]["relaysensor"]?).not_nil!.as_h
+ logger.debug { "relays = #{relays}" }
+ connector = relays[index.to_s]
+ do_send("setstate,#{connector},#{state ? 1 : 0}", **options)
+ else
+ logger.warn { "Attempted to set relay on GlobalCache that does not exist: #{index}" }
+ end
+ end
+
+ def ir(index : Int32, command : String, **options)
+ do_send("sendir,1:#{index},#{command}", **options)
+ end
+
+ enum IrMode
+ IR
+ SENSOR
+ SENSOR_NOTIFY
+ IR_NOCARRIER
+ end
+
+ def set_ir(index : Int32, mode : IrMode, **options)
+ if index < self[:num_ir].as_i
+ connector = self[:relay_config]["ir"][index.to_s]
+ do_send("set_IR,#{connector},#{mode}", **options)
+ else
+ logger.warn { "Attempted to set IR mode on GlobalCache that does not exist: #{index}" }
+ end
+ end
+
+ def relay_status?(index : Int32, **options)
+ if index < self[:num_relays].as_i
+ connector = self[:relay_config]["relay"][index.to_s]
+ do_send("getstate,#{connector}", **options)
+ else
+ logger.warn { "Attempted to check IO on GlobalCache that does not exist: #{index}" }
+ end
+ end
+
+ def ir_status?(index : Int32, **options)
+ if index < self[:num_ir].as_i
+ connector = self[:relay_config]["ir"][index.to_s]
+ do_send("getstate,#{connector}", **options)
+ else
+ logger.warn { "Attempted to check IO on GlobalCache that does not exist: #{index}" }
+ end
+ end
+
+ def received(data, task)
+ # Remove the delimiter
+ data = String.new(data[0..-2])
+ logger.debug { "GlobalCache sent #{data}" }
+ data = data.split(',')
+ task_name = task.try &.name || "unknown"
+
+ case data[0]
+ when "state", "statechange"
+ type, index = self[:port_config][data[1]]
+ self["#{type}#{index}"] = data[2] == "1" # Is relay index on?
+ when "device"
+ address = data[1]
+ number, type = data[2].split(' ') # The response was "device,2,3 RELAY"
+
+ type = type.downcase
+
+ @relay_config[type] ||= {} of Int32 => String
+ current = @relay_config[type].size
+
+ (current..(current + number.to_i - 1)).each_with_index(1) do |i, dev_index|
+ port = "#{address}:#{dev_index}"
+ @relay_config[type][i] = port
+ @port_config[port] = {type, i}
+ end
+
+ return task.try &.success
+ when "endlistdevices"
+ self[:num_relays] = @relay_config["relay"].size if @relay_config["relay"]?
+ if @relay_config["relaysensor"]?
+ @relay_config["relaysensor"][1] = "1:2"
+ @relay_config["relaysensor"][2] = "1:3"
+ @relay_config["relaysensor"][3] = "1:4"
+ self[:num_relays] = @relay_config["relaysensor"].size
+ end
+ self[:num_ir] = @relay_config["ir"].size if @relay_config["ir"]?
+ self[:relay_config] = @relay_config
+ self[:port_config] = @port_config
+ logger.debug { "self[:relay_config] is #{self[:relay_config]}" }
+ logger.debug { "self[:port_config] is #{self[:port_config]}" }
+ @relay_config = {} of String => Hash(Int32, String)
+ @port_config = {} of String => Tuple(String, Int32)
+ self[:config_indexed] = true
+
+ return task.try &.success
+ end
+
+ if data.size == 1
+ error = case data[0].split(' ')[1].to_i
+ when 1 then "Command was missing the carriage return delimiter"
+ when 2 then "Invalid module address when looking for version"
+ when 3 then "Invalid module address"
+ when 4 then "Invalid connector address"
+ when 5 then "Connector address 1 is set up as \"sensor in\" when attempting to send an IR command"
+ when 6 then "Connector address 2 is set up as \"sensor in\" when attempting to send an IR command"
+ when 7 then "Connector address 3 is set up as \"sensor in\" when attempting to send an IR command"
+ when 8 then "Offset is set to an even transition number, but should be set to an odd transition number in the IR command"
+ when 9 then "Maximum number of transitions exceeded (256 total on/off transitions allowed)"
+ when 10 then "Number of transitions in the IR command is not even (the same number of on and off transitions is required)"
+ when 11 then "Contact closure command sent to a module that is not a relay"
+ when 12 then "Missing carriage return. All commands must end with a carriage return"
+ when 13 then "State was requested of an invalid connector address, or the connector is programmed as IR out and not sensor in."
+ when 14 then "Command sent to the unit is not supported by the GC-100"
+ when 15 then "Maximum number of IR transitions exceeded"
+ when 16 then "Invalid number of IR transitions (must be an even number)"
+ when 21 then "Attempted to send an IR command to a non-IR module"
+ when 23 then "Command sent is not supported by this type of module"
+ else "Unknown error"
+ end
+ return task.try &.abort("GlobalCache error for command #{task_name}: #{error}")
+ end
+
+ task.try &.success
+ end
+
+ private def do_send(command : String, **options)
+ logger.debug { "-- GlobalCache, sending: #{command}" }
+ command = "#{command}#{DELIMITER}"
+ send(command, **options)
+ end
+end
diff --git a/drivers/global_cache/gc_100_spec.cr b/drivers/global_cache/gc_100_spec.cr
new file mode 100644
index 00000000000..1c7129ca869
--- /dev/null
+++ b/drivers/global_cache/gc_100_spec.cr
@@ -0,0 +1,44 @@
+DriverSpecs.mock_driver "GlobalCache::Gc100" do
+ # connected
+ # get_devices
+ should_send("getdevices\r")
+ responds("device,2,3 RELAY\r")
+ responds("device,1,2 RELAYSENSOR\r")
+ responds("device,3,1 IR\r")
+ responds("endlistdevices\r")
+ should_send("get_NET,0:1\r")
+
+ sleep 1
+
+ status[:relay_config].should eq({
+ "relay" => {"0" => "2:1", "1" => "2:2", "2" => "2:3"},
+ "relaysensor" => {"0" => "1:1", "1" => "1:2", "2" => "1:3", "3" => "1:4"},
+ "ir" => {"0" => "3:1"},
+ })
+ status[:port_config].should eq({
+ "2:1" => ["relay", 0], "2:2" => ["relay", 1], "2:3" => ["relay", 2], "1:1" => ["relaysensor", 0], "1:2" => ["relaysensor", 1], "3:1" => ["ir", 0],
+ })
+
+ exec(:relay, true, 1)
+ should_send("setstate,2:2,1\r")
+ responds("state,2:2,1\r")
+ status[:relay1].should eq(true)
+
+ exec(:ir, 0, "4444")
+ should_send("sendir,1:0,4444\r")
+ responds("completeir,1:0,4444\r")
+
+ exec(:set_ir, 0, "ir")
+ should_send("set_IR,3:1,IR\r")
+ responds("TODO 1\r")
+
+ exec(:relay_status?, 2)
+ should_send("getstate,2:3\r")
+ responds("state,2:3,0\r")
+ status[:relay2].should eq(false)
+
+ exec(:ir_status?, 0)
+ should_send("getstate,3:1\r")
+ responds("state,3:1,1\r")
+ status[:ir0].should eq(true)
+end
diff --git a/drivers/helvar/helvar_net_protocol.md b/drivers/helvar/helvar_net_protocol.md
new file mode 100644
index 00000000000..6ec61f6a72a
--- /dev/null
+++ b/drivers/helvar/helvar_net_protocol.md
@@ -0,0 +1,95 @@
+
+# Helvar.net Protocol
+
+Reference: https://aca.im/driver_docs/Helvar/HelvarNet-Overview.pdf
+
+For use with Helvar to DALI routers
+
+* TCP port: 50000
+* UDP port: 50001
+
+
+## Addressing
+
+The Helvar lighting router system would consist of a number of routers (910 or 920) that enable
+connection to a variety of different inputs and outputs using a different data buses.
+
+The backbone structure of the system uses Ethernet Cat 5 cabling & the TCP/IP protocol. As
+such each system (or workgroup) is a cluster of routers. The cluster (3rd Octet in IP addressing)
+forms the first part of the unique device address
+
+Each router within the system will then have a unique IP address with the 4th octet providing the
+unique router number. This number forms the second digit of the unique device address.
+
+The cluster.router is then followed by a subnet. The subnet refers to the data bus on which
+inputs or output devices are connected. Depending on the router type (910 or 920) there are 2
+or 4 subnets available. In both case’s subnet 1 & 2 use the DALI protocol. For the 920 you have
+additional subnets 3 (using S-Dim) and 4 (DMX).
+
+Following cluster.router.subnet is then the device address. This number is limited by the type of
+subnet to which the device is connected and in the case of output devices completes the device
+address.
+
+For input devices there is a further sub-device which will refer to a particular property of that
+input device for example a control panel (device) would have a number of buttons (sub-device).
+
+So a full address would be written:-
+
+* Cluster (1..253), Router (1..254), Subnet (1..4), Device (1..255), Subdevice (1..16)
+* cluster.router.subnet.address for output devices
+* cluster.router.subnet.address for input devices
+* cluster.router.subnet.address.sub-address for input sub-devices
+
+### Address Structure
+
+* Cluster = the 3rd octet of the IP address range used
+* Router = the 4th octet of the IP address of that particular router
+* Subnet = the data bus on which devices are connected (Dali 1 = 1, Dali 2 = 2, S-Dim = 3, DMX = 4)
+* Address = the device address, dependant on the data bus (Dali = 1-64, S-Dim = 1-252, DMX = 1-512)
+* Sub-address = the sub-device of the device (button, sensor, input etc.)
+
+
+## Commands
+
+* `>V:` is the command prefix (`>V:2` represents the protocol version 2)
+* `C:` is the command type
+ * `11` == select scene
+ * `13` == direct level group address
+ * `14` == direct level short address
+ * `109` == query selected scene
+* `G:` specifies the lighting group
+* `S:` specifices the lighting scene
+* `F:` specifies the fade time (in 1/100ths of a second. So a fade of 900 is 9 seconds)
+* `L:` specifies the level (between 1 and 100)
+* `@` specifies the short address (looks like: 1.2.1.1)
+* all commands end with a `#`
+
+
+### Example Commands
+
+* Direct level, short address: `>V:1,C:14,L:{0},F:{1},@{2}#`
+ * {0} == level, {1} == fade_time, {2} == address
+* Direct level, group address: `>V:1,C:13,G:{0},L:{1},F:{2}#`
+ * {0} == address, {1} == level, {2} == fade_time
+* Keep socket alive: `>V:1,C:14,L:0,F:9000,@65#`
+ * Write to dummy address to keep socket alive
+
+
+### Example Query
+
+* `>V:2,C:109,G:17#` query Group 17 as to which scene it is currently in
+ * responds with: `?V:2,C:109,G:17=14#`
+ * i.e. Group 17 is in scene 14
+
+### Example Error
+
+* `>V:1,C:104,@:2.2.1.1#` query device type
+ * responds with: `!V:1,C:104,@:2.2.1.1=11#`
+ * i.e. error 11, device does not exist
+
+
+References:
+
+* https://github.com/tkln/HelvarNet/blob/master/helvar.py
+* https://github.com/houmio/houmio-driver-helvar-router/blob/master/src/driver.coffee
+
diff --git a/drivers/helvar/net.cr b/drivers/helvar/net.cr
new file mode 100755
index 00000000000..e0e76899d12
--- /dev/null
+++ b/drivers/helvar/net.cr
@@ -0,0 +1,290 @@
+module Helvar; end
+
+# Documentation: https://aca.im/driver_docs/Helvar/HelvarNet-Overview.pdf
+
+class Helvar::Net < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 50000
+ descriptive_name "Helvar Net Lighting Gateway"
+ generic_name :Lighting
+
+ default_settings({
+ version: 2,
+ ignore_blocks: true,
+ poll_group: nil,
+ })
+
+ def on_load
+ transport.tokenizer = Tokenizer.new("#")
+ on_update
+ end
+
+ def on_update
+ @version = setting?(Int32, :version) || 2
+ @ignore_blocks = setting?(Bool, :ignore_blocks) || true
+ @poll_group = setting?(Int32, :poll_group)
+ end
+
+ @poll_group : Int32?
+
+ def connected
+ schedule.every(40.seconds) do
+ logger.debug { "-- Polling Helvar" }
+ if poll_group = @poll_group
+ get_current_preset poll_group
+ else
+ query_software_version
+ end
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def lighting(group : Int32, state : Bool)
+ level = state ? 100 : 0
+ light_level(group, level)
+ end
+
+ def light_level(group : Int32, level : Int32, fade : Int32 = 1000)
+ fade = (fade / 10).to_i
+ self["area#{group}_level"] = level
+ group_level(group: group, level: level, fade: fade, name: "group_level#{group}")
+ end
+
+ def trigger(group : Int32, scene : Int32, fade : Int32 = 1000)
+ fade = (fade / 10).to_i
+ self["area#{group}"] = scene
+ group_scene(group: group, scene: scene, fade: fade, name: "group_scene#{group}")
+ end
+
+ def get_current_preset(group : Int32)
+ query_last_scene(group: group)
+ end
+
+ CMD_METHODS = {
+ group_scene: 11,
+ device_scene: 12,
+ group_level: 13,
+ device_level: 14,
+ group_proportion: 15,
+ device_proportion: 16,
+ group_modify_proportion: 17,
+ device_modify_proportion: 18,
+ group_emergency_test: 19,
+ device_emergency_test: 20,
+ group_emergency_duration_test: 21,
+ device_emergency_duration_test: 22,
+ group_emergency_stop: 23,
+ device_emergency_stop: 24,
+
+ # Query commands
+ query_lamp_hours: 70,
+ query_ballast_hours: 71,
+ query_max_voltage: 72,
+ query_min_voltage: 73,
+ query_max_temp: 74,
+ query_min_temp: 75,
+ query_device_types_with_addresses: 100,
+ query_clusters: 101,
+ query_routers: 102,
+ query_LSIB: 103,
+ query_device_type: 104,
+ query_description_group: 105,
+ query_description_device: 106,
+ query_workgroup_name: 107, # must use UDP
+ query_workgroup_membership: 108,
+ query_last_scene: 109,
+ query_device_state: 110,
+ query_device_disabled: 111,
+ query_lamp_failure: 112,
+ query_device_faulty: 113,
+ query_missing: 114,
+ query_emergency_battery_failure: 129,
+ query_measurement: 150,
+ query_inputs: 151,
+ query_load: 152,
+ query_power_consumption: 160,
+ query_group_power_consumption: 161,
+ query_group: 164,
+ query_groups: 165,
+ query_scene_names: 166,
+ query_scene_info: 167,
+ query_emergency_func_test_time: 170,
+ query_emergency_func_test_state: 171,
+ query_emergency_duration_time: 172,
+ query_emergency_duration_state: 173,
+ query_emergency_battery_charge: 174,
+ query_emergency_battery_time: 175,
+ query_emergency_total_lamp_time: 176,
+ query_time: 185,
+ query_longitude: 186,
+ query_latitude: 187,
+ query_time_zone: 188,
+ query_daylight_savings: 189,
+ query_software_version: 190,
+ query_helvar_net: 191,
+ }
+
+ # Dynamically define methods based on the tuple above
+ {% for name, command in CMD_METHODS %}
+ def {{name.id}}(group : Int32? = nil, block : Int32? = nil, level : Int32? = nil, scene : Int32? = nil, fade : Int32? = nil, addr : Int32? = nil, **options)
+ do_send({{command.id.stringify}}, @version, group, block, level, scene, fade, addr, **options)
+ end
+ {% end %}
+
+ # Generate a String => String hash based on the data above
+ macro build_command_hash
+ COMMANDS = {
+ {% for name, command in CMD_METHODS %}
+ {{name.id.stringify}} => {{command.id.stringify}},
+ {% end %}
+ }
+ COMMANDS.merge!(COMMANDS.invert)
+ end
+
+ build_command_hash
+
+ PARAMS = {
+ "V" => :ver,
+ "Q" => :seq,
+ "C" => :cmd,
+ "A" => :ack,
+ "@" => :addr,
+ "F" => :fade,
+ "T" => :time,
+ "L" => :level,
+ "G" => :group,
+ "S" => :scene,
+ "B" => :block,
+ "N" => :latitude,
+ "E" => :longitude,
+ "Z" => :time_zone,
+ # brighter or dimmer than the current level by a % of the difference
+ "P" => :proportion,
+ "D" => :display_screen,
+ "Y" => :daylight_savings,
+ "O" => :force_store_scene,
+ "K" => :constant_light_scene,
+ }
+
+ def received(data, task)
+ data = String.new(data)
+ logger.debug { "Helvar sent: #{data}" }
+
+ # Remove the # at the end of the message
+ data = data[0..-2]
+
+ # Group level changed: ?V:2,C:109,G:12706=13 (query scene response)
+ # Update pushed >V:2,C:11,G:25007,B:1,S:13,F:100 (current scene level)
+
+ # Remove junk data (when whitelisting gateway is in place)
+ start_of_message = data.index(/[\?\>\!]V:/i)
+ if start_of_message != 0
+ logger.warn { "Lighting error response: #{data[0...start_of_message]}" }
+ data = data[start_of_message..-1]
+ end
+
+ # remove connectors from multi-part responses
+ data = data.delete("$")
+
+ indicator = data[0]
+ case indicator
+ when '?', '>'
+ # remove indicator
+ data = data[1..-1]
+
+ # check if this is a result
+ parts = data.split("=")
+ data = parts[0]
+ value = parts[1]?
+
+ # Extract components of the message
+ params = {} of Symbol => String
+ data.split(",").each do |param|
+ parts = param.split(":")
+ if parts.size > 1
+ params[PARAMS[parts[0]]] = parts[1]
+ elsif parts[0][0] == '@'
+ params[:addr] == parts[0][1..-1]
+ else
+ logger.debug { "unknown param type #{param}" }
+ end
+ end
+
+ # Check for :ack
+ ack = params[:ack]?
+ if ack
+ return task.try &.abort("request failed") if ack != "1"
+ return task.try &.success
+ end
+
+ cmd = COMMANDS[params[:cmd]]
+ case cmd
+ when "query_last_scene"
+ self["area#{params[:group]}"] = value.try &.to_i
+ when "group_scene"
+ block = params[:block]
+ if block
+ if @ignore_blocks
+ self["area#{params[:group]}"] = params[:scene].to_i
+ else
+ self["area#{params[:group]}_block#{block}"] = params[:scene].to_i
+ end
+ else
+ self["area#{params[:group]}"] = params[:scene].to_i
+ end
+ else
+ logger.debug { "unknown response value\n#{cmd} = #{value}" }
+ end
+ when '!'
+ error = ERRORS[data.split("=")[1]]
+ error = "#{error} for #{data}"
+ self[:last_error] = error
+ logger.warn { error }
+ return task.try &.abort(error)
+ else
+ logger.info { "unknown request #{data}" }
+ end
+
+ task.try &.success
+ end
+
+ ERRORS = {
+ "0" => "success",
+ "1" => "invalid group index parameter",
+ "2" => "invalid cluster parameter",
+ "3" => "invalid router",
+ "4" => "invalid router subnet",
+ "5" => "invalid device parameter",
+ "6" => "invalid sub device parameter",
+ "7" => "invalid block parameter",
+ "8" => "invalid scene",
+ "9" => "cluster does not exist",
+ "10" => "router does not exist",
+ "11" => "device does not exist",
+ "12" => "property does not exist",
+ "13" => "invalid RAW message size",
+ "14" => "invalid messages type",
+ "15" => "invalid message command",
+ "16" => "missing ASCII terminator",
+ "17" => "missing ASCII parameter",
+ "18" => "incompatible version",
+ }
+
+ protected def do_send(cmd : String, ver = @version, group = nil, block = nil, level = nil, scene = nil, fade = nil, addr = nil, **options)
+ req = String.build do |str|
+ str << ">V:" << ver << ",C:" << cmd
+ str << ",G:" << group if group
+ str << ",B:" << block if block
+ str << ",L:" << level if level
+ str << ",S:" << scene if scene
+ str << ",F:" << fade if fade
+ str << ",@:" << addr if addr
+ str << "#"
+ end
+ logger.debug { "Requesting helvar: #{req}" }
+ send(req, **options)
+ end
+end
diff --git a/drivers/helvar/net_spec.cr b/drivers/helvar/net_spec.cr
new file mode 100644
index 00000000000..f4bea2e5fe0
--- /dev/null
+++ b/drivers/helvar/net_spec.cr
@@ -0,0 +1,25 @@
+DriverSpecs.mock_driver "Helvar::Net" do
+ # Perform actions
+ resp = exec(:trigger, group: 1, scene: 2, fade: 1100)
+ should_send(">V:2,C:11,G:1,S:2,F:110#")
+ responds(">V:2,C:11,G:1,S:2,F:110,A:1#")
+ resp.get
+ status[:area1].should eq(2)
+
+ resp = exec(:get_current_preset, group: 17)
+ should_send(">V:2,C:109,G:17#")
+ responds("?V:2,C:109,G:17=14#")
+ resp.get
+ status[:area17].should eq(14)
+
+ resp = exec(:get_current_preset, group: 20)
+ should_send(">V:2,C:109,G:20#")
+ responds("!V:2,C:109,G:20=1#")
+ expect_raises(PlaceOS::Driver::RemoteException, "invalid group index parameter for !V:2,C:109,G:20=1 (Abort)") do
+ resp.get
+ end
+ status[:last_error].should eq("invalid group index parameter for !V:2,C:109,G:20=1")
+
+ transmit(">V:2,C:11,G:2001,B:1,S:1,F:100#")
+ status[:area2001].should eq(1)
+end
diff --git a/drivers/hitachi/projector/cp_tw_series_basic.cr b/drivers/hitachi/projector/cp_tw_series_basic.cr
new file mode 100644
index 00000000000..6d2df9f8ab2
--- /dev/null
+++ b/drivers/hitachi/projector/cp_tw_series_basic.cr
@@ -0,0 +1,243 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+
+class Hitachi::Projector::CpTwSeriesBasic < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ # Discovery Information
+ tcp_port 23
+ descriptive_name "Hitachi CP-TW Projector (no auth)"
+ generic_name :Display
+
+ @recover_power : PlaceOS::Driver::Proxy::Scheduler::TaskWrapper? = nil
+ @recover_input : PlaceOS::Driver::Proxy::Scheduler::TaskWrapper? = nil
+ # nil by default (allows manual on and off)
+ @power_target : Bool? = nil
+ @input_target : Input? = nil
+
+ def on_load
+ # Response time is slow
+ # and as a make break device it may take time
+ # to actually setup the connection with the projector
+ queue.delay = 100.milliseconds
+ queue.timeout = 5.seconds
+ queue.retries = 3
+
+ # Meta data for inquiring interfaces
+ self[:type] = :projector
+ end
+
+ def connected
+ schedule.every(50.seconds, true) { poll_1 }
+ schedule.every(10.minutes, true) { poll_2 }
+ end
+
+ def poll_1
+ power?(priority: 0).get
+ if self[:power]?.try &.as_bool
+ input?(priority: 0)
+ audio_mute?(priority: 0)
+ video_mute?(priority: 0)
+ freeze?(priority: 0)
+ end
+ end
+
+ def poll_2
+ lamp?(priority: 0)
+ filter?(priority: 0)
+ error?(priority: 0)
+ end
+
+ def disconnected
+ schedule.clear
+ @recover_power = nil
+ @recover_input = nil
+ end
+
+ def power(state : Bool)
+ @power_target = state
+ if state
+ logger.debug { "requested to power on" }
+ do_send(:power_on)
+ else
+ logger.debug { "requested to power off" }
+ do_send(:power_off)
+ end
+ power?
+ end
+
+ def switch_to(input : Input)
+ @input_target = input
+ do_send(input.to_s.downcase)
+ input?
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ mute_video(state) if layer.video? || layer.audio_video?
+ mute_audio(state) if layer.audio? || layer.audio_video?
+ end
+
+ def mute_video(state : Bool = true)
+ if state
+ do_send(:mute_video)
+ else
+ do_send(:unmute_video)
+ end
+ video_mute?
+ end
+
+ def mute_audio(state : Bool = true)
+ if state
+ do_send(:mute_audio)
+ else
+ do_send(:unmute_audio)
+ end
+ audio_mute?
+ end
+
+ def lamp_hours_reset
+ do_send(:lamp_hours_reset)
+ lamp?
+ end
+
+ def filter_hours_reset
+ do_send(:filter_hours_reset)
+ filter?
+ end
+
+ enum Response
+ Ack = 0x06
+ Nak = 0x15
+ Error = 0x1c
+ Data = 0x1d
+ Busy = 0x1f
+ end
+
+ enum Input
+ Hdmi = 0x03
+ Hdmi2 = 0x0d
+ HdbaSet = 0x11
+ end
+
+ enum Error
+ Normal
+ Cover
+ Fan
+ Lamp
+ Temp
+ AirFlow
+ Cold
+ Filter
+ end
+
+ def received(data, task)
+ logger.debug { "received 0x#{data}" }
+ command = task.try &.name
+
+ case Response.from_value(data[0])
+ when .ack?
+ task.try &.success
+ when .nak?
+ task.try &.abort("NAK response")
+ when .error?
+ task.try &.abort("Error response")
+ when .data?
+ if command
+ case command
+ when "power?"
+ self[:power] = data[1] == 1
+ self[:cooling] = data[1] == 2
+
+ if self[:power]? == @power_target
+ @power_target = nil
+ elsif @power_target && @recover_power.nil?
+ logger.debug { "recovering power state #{self[:power]} != target #{@power_target}" }
+ @recover_power = schedule.in(3.seconds) do
+ @recover_power = nil
+ power(@power_target.not_nil!)
+ end
+ end
+ when "input?"
+ input = Input.from_value?(data[1])
+ self[:input] = input || "unknown"
+ if @input_target
+ if input == @input_target
+ @input_target = nil
+ elsif @recover_input.nil?
+ logger.debug { "recovering input #{self[:input]} != target #{@input_target}" }
+ @recover_input = schedule.in(3.seconds) do
+ @recover_input = nil
+ switch_to(@input_target.not_nil!)
+ end
+ end
+ end
+ when "error?"
+ self[:error_status] = Error.from_value?(data[1]) || "unknown"
+ when "freeze?"
+ self[:frozen] = data[1] == 1
+ when "audio_mute?"
+ self[:audio_mute] = data[1] == 1
+ when "video_mute?"
+ self[:video_mute] = data[1] == 1
+ when "lamp?"
+ self[:lamp] = data[1] * data[2]
+ when "filter?"
+ self[:filter] = data[1] * data[2]
+ end
+ task.try &.success
+ else
+ task.try &.abort("data received for unknown command")
+ end
+ when .busy?
+ if data[1] == 4 && data[2] == 0
+ task.try &.abort("authentication enabled, please disable")
+ else
+ task.try &.retry("projector busy, retrying")
+ end
+ end
+ end
+
+ # Note: commands have spaces in between each byte for readability
+ Commands = {
+ # SetRequests
+ power_on: "BA D2 01 00 00 60 01 00",
+ power_off: "2A D3 01 00 00 60 00 00",
+ hdmi: "0E D2 01 00 00 20 03 00",
+ hdmi2: "6E D6 01 00 00 20 0D 00",
+ mute_video: "6E F1 01 00 A0 20 01 00",
+ unmute_video: "FE F0 01 00 A0 20 00 00",
+ mute_audio: "D6 D2 01 00 02 20 01 00",
+ unmute_audio: "46 D3 01 00 02 20 00 00",
+ lamp_hours_reset: "58 DC 06 00 30 70 00 00",
+ filter_hours_reset: "98 C6 06 00 40 70 00 00",
+ # GetRequests
+ power?: "19 D3 02 00 00 60 00 00",
+ input?: "CD D2 02 00 00 20 00 00",
+ error?: "D9 D8 02 00 20 60 00 00",
+ freeze?: "B0 D2 02 00 02 30 00 00",
+ audio_mute?: "75 D3 02 00 02 20 00 00",
+ video_mute?: "CD F0 02 00 A0 20 00 00",
+ lamp?: "C2 FF 02 00 90 10 00 00",
+ filter?: "C2 F0 02 00 A0 10 00 00",
+ }
+
+ GetRequests = %i(power? input? error? freeze? audio_mute? video_mute? lamp? filter?)
+ {% for name in GetRequests %}
+ @[Security(Level::Administrator)]
+ def {{name.id}}(**options)
+ do_send({{name.id.stringify}}, **options)
+ end
+ {% end %}
+
+ private def do_send(cmd, **options)
+ data = "BEEF030600 #{Commands[cmd]}"
+ logger.debug { "requesting \"0x#{data}\" name: #{cmd}" }
+ # Remove spaces that have been added for readability
+ send(data.delete(' ').hexbytes, **options, name: cmd)
+ end
+end
diff --git a/drivers/hitachi/projector/cp_tw_series_basic_spec.cr b/drivers/hitachi/projector/cp_tw_series_basic_spec.cr
new file mode 100644
index 00000000000..83a377534be
--- /dev/null
+++ b/drivers/hitachi/projector/cp_tw_series_basic_spec.cr
@@ -0,0 +1,80 @@
+require "placeos-driver"
+require "./cp_tw_series_basic"
+
+DriverSpecs.mock_driver "Hitachi::Projector::CpTwSeriesBasic" do
+ c = Hitachi::Projector::CpTwSeriesBasic::Commands
+
+ # connected
+ # power?
+ should_send("BEEF030600#{c[:power?]}".delete(' ').hexbytes)
+ responds("\x1d\x01\x00")
+ status[:power].should eq(true)
+ # lamp?
+ should_send("BEEF030600#{c[:lamp?]}".delete(' ').hexbytes)
+ responds("\x1d\x03\x01")
+ status[:lamp].should eq(3)
+ # filter?
+ should_send("BEEF030600#{c[:filter?]}".delete(' ').hexbytes)
+ responds("\x1d\x04\x01")
+ status[:filter].should eq(4)
+ # error?
+ should_send("BEEF030600#{c[:error?]}".delete(' ').hexbytes)
+ responds("\x1d\x00\x00")
+ status[:error_status].should eq("Normal")
+ # input?
+ should_send("BEEF030600#{c[:input?]}".delete(' ').hexbytes)
+ responds("\x1d\x0d\x00")
+ status[:input].should eq("Hdmi2")
+ # audio_mute?
+ should_send("BEEF030600#{c[:audio_mute?]}".delete(' ').hexbytes)
+ responds("\x1d\x01\x00")
+ status[:audio_mute].should eq(true)
+ # video_mute?
+ should_send("BEEF030600#{c[:video_mute?]}".delete(' ').hexbytes)
+ responds("\x1d\x01\x00")
+ status[:video_mute].should eq(true)
+ # freeze?
+ should_send("BEEF030600#{c[:freeze?]}".delete(' ').hexbytes)
+ responds("\x1d\x01\x00")
+ status[:frozen].should eq(true)
+
+ exec(:mute, false)
+ should_send("BEEF030600#{c[:unmute_video]}".delete(' ').hexbytes)
+ responds("\x06")
+ should_send("BEEF030600#{c[:video_mute?]}".delete(' ').hexbytes)
+ responds("\x1d\x00\x00")
+ status[:video_mute].should eq(false)
+ should_send("BEEF030600#{c[:unmute_audio]}".delete(' ').hexbytes)
+ responds("\x06")
+ should_send("BEEF030600#{c[:audio_mute?]}".delete(' ').hexbytes)
+ responds("\x1d\x00\x00")
+ status[:video_mute].should eq(false)
+
+ exec(:switch_to, "hdmi")
+ should_send("BEEF030600#{c[:hdmi]}".delete(' ').hexbytes)
+ responds("\x06")
+ should_send("BEEF030600#{c[:input?]}".delete(' ').hexbytes)
+ responds("\x1d\x03\x00")
+ status[:input].should eq("Hdmi")
+
+ exec(:lamp_hours_reset)
+ should_send("BEEF030600#{c[:lamp_hours_reset]}".delete(' ').hexbytes)
+ responds("\x06")
+ should_send("BEEF030600#{c[:lamp?]}".delete(' ').hexbytes)
+ responds("\x1d\x00\x00")
+ status[:lamp].should eq(0)
+
+ exec(:filter_hours_reset)
+ should_send("BEEF030600#{c[:filter_hours_reset]}".delete(' ').hexbytes)
+ responds("\x06")
+ should_send("BEEF030600#{c[:filter?]}".delete(' ').hexbytes)
+ responds("\x1d\x00\x00")
+ status[:filter].should eq(0)
+
+ exec(:power, false)
+ should_send("BEEF030600#{c[:power_off]}".delete(' ').hexbytes)
+ responds("\x06")
+ should_send("BEEF030600#{c[:power?]}".delete(' ').hexbytes)
+ responds("\x1d\x00\x00")
+ status[:power].should eq(false)
+end
diff --git a/drivers/lenel/open_access.cr b/drivers/lenel/open_access.cr
new file mode 100644
index 00000000000..9d309ba13b4
--- /dev/null
+++ b/drivers/lenel/open_access.cr
@@ -0,0 +1,227 @@
+require "placeos-driver"
+require "time"
+
+class Lenel::OpenAccess < PlaceOS::Driver; end
+
+require "./open_access/client"
+
+class Lenel::OpenAccess < PlaceOS::Driver
+ include OpenAccess::Models
+
+ generic_name :Security
+ descriptive_name "Lenel OpenAccess"
+ description "Bindings for Lenel OnGuard physical security system"
+ uri_base "https://example.com/api/access/onguard/openaccess"
+ default_settings({
+ application_id: "",
+ directory_id: "",
+ username: "",
+ password: "",
+ })
+
+ private getter client : OpenAccess::Client do
+ transport = PlaceOS::HTTPClient.new self
+ app_id = setting String, :application_id
+ OpenAccess::Client.new transport, app_id
+ end
+
+ def on_load
+ schedule.every 5.minutes, &->check_comms
+ end
+
+ def on_update
+ logger.debug { "settings updated" }
+ client.app_id = setting String, :application_id
+ authenticate!
+ end
+
+ def connected
+ logger.debug { "connected" }
+ authenticate! if client.token.nil?
+ end
+
+ def disconnected
+ logger.debug { "disconnected" }
+ client.token = nil
+ end
+
+ private def authenticate! : Nil
+ username = setting String, :username
+ password = setting String, :password
+ directory = setting?(String, :directory_id).presence
+
+ logger.debug { "requesting access token for #{username}" }
+
+ begin
+ auth = client.login username, password, directory
+ client.token = auth[:session_token]
+
+ renewal_time = auth[:token_expiration_time] - 5.minutes
+ schedule.at renewal_time, &->authenticate!
+
+ logger.info { "authenticated - renews at #{renewal_time}" }
+
+ set_connected_state true
+ rescue e
+ logger.error { "authentication failed: #{e.message}" }
+ set_connected_state false
+ end
+ end
+
+ # Test service connectivity.
+ @[Security(Level::Support)]
+ def check_comms
+ logger.debug { "checking service connectivity" }
+ if client.token
+ client.keepalive
+ logger.info { "client online and authenticated" }
+ else
+ client.version
+ logger.warn { "service reachable, no active auth session" }
+ authenticate!
+ end
+ rescue e : OpenAccess::Error
+ logger.error { e.message }
+ set_connected_state false
+ end
+
+ # Query the directories available for auth.
+ @[Security(Level::Support)]
+ def list_directories
+ client.directories
+ end
+
+ # Gets the version of the attached OnGuard system.
+ @[Security(Level::Support)]
+ def version
+ client.version
+ end
+
+ # Query the available badge types.
+ #
+ # Badge types contain default configuration that is applied to any badge
+ # created under them. This includes items such as access areas, activation
+ # windows and other bulk config. These may then be override on individual
+ # badge instances.
+ @[Security(Level::Support)]
+ def badge_types
+ client.lookup BadgeType
+ end
+
+ # Creates a new badge of the specied *type*, belonging to *personid* with a
+ # specific *id*.
+ #
+ # Note: 'id' is the physical badge number (e.g. the ID written to an NFC chip)
+ @[Security(Level::Administrator)]
+ def create_badge(
+ type : Int32,
+ id : Int64,
+ personid : Int32,
+ uselimit : Int32? = nil,
+ activate : Time? = nil,
+ deactivate : Time? = nil
+ )
+ logger.debug { "creating badge badge for cardholder #{personid}" }
+ client.create Badge, **args
+ end
+
+ # Deletes a badge with the specified *badgekey*.
+ @[Security(Level::Administrator)]
+ def delete_badge(badgekey : Int32) : Nil
+ logger.debug { "deleting badge #{badgekey}" }
+ client.delete Badge, **args
+ end
+
+ # Lookup a cardholder by *email* address.
+ @[Security(Level::Support)]
+ def lookup_cardholder(email : String)
+ cardholders = client.lookup Cardholder, filter: %(email = "#{email}")
+ if cardholders.size > 1
+ logger.warn { "duplicate records exist for #{email}" }
+ end
+ cardholders.first?
+ end
+
+ # Creates a new cardholder.
+ #
+ # An error will be returned if an existing cardholder exists for the specified
+ # *email* address.
+ @[Security(Level::Support)]
+ def create_cardholder(
+ email : String,
+ firstname : String,
+ lastname : String
+ )
+ logger.debug { "creating cardholder record for #{email}" }
+ unless client.count(Cardholder, filter: %(email = "#{email}")).zero?
+ raise ArgumentError.new "record already exists for #{email}"
+ end
+ client.create Cardholder, **args
+ end
+
+ # Deletes a cardholed by their person *id*.
+ @[Security(Level::Administrator)]
+ def delete_cardholder(id : Int32) : Nil
+ logger.debug { "deleting cardholder #{id}" }
+ client.delete Cardholder, **args
+ end
+end
+
+################################################################################
+#
+# Warning: nasty hacks below. These are intended as a _temporary_ measure to
+# modify the behaviour of the driver framework as a POC.
+#
+# The intent is to provide a `HTTP::Client`-ish object that uses the underlying
+# queue and config. This provides a familiar interface for users, but
+# importantly also allows it to be passed as a compatible object to client libs
+# that may already exist for the service being integrated.
+#
+
+abstract class PlaceOS::Driver::Transport
+ def before_request(&callback : HTTP::Request ->)
+ before_request = @before_request ||= [] of (HTTP::Request ->)
+ before_request << callback
+ end
+
+ private def install_middleware(client : HTTP::Client)
+ client.before_request do |req|
+ @before_request.try &.each &.call(req)
+ end
+ end
+end
+
+class PlaceOS::Driver::TransportTCP
+ def new_http_client(uri, context)
+ previous_def.tap &->install_middleware(HTTP::Client)
+ end
+end
+
+class PlaceOS::Driver::TransportHTTP
+ def new_http_client(uri, context)
+ previous_def.tap &->install_middleware(HTTP::Client)
+ end
+end
+
+class PlaceOS::HTTPClient < HTTP::Client
+ def initialize(@driver : PlaceOS::Driver)
+ @host = ""
+ @port = -1
+ end
+
+ delegate get, post, put, patch, delete, to: @driver
+
+ def before_request(&block : HTTP::Request ->)
+ @driver.transport.before_request &block
+ end
+end
+
+# Patch in support for `body` in DELETE requests
+class PlaceOS::Driver
+ protected def delete(path, body : ::HTTP::Client::BodyType = nil,
+ params : Hash(String, String?) = {} of String => String?,
+ headers : Hash(String, String) | HTTP::Headers = HTTP::Headers.new,
+ secure = false, concurrent = false)
+ transport.http("DELETE", path, body, params, headers, secure, concurrent)
+ end
+end
diff --git a/drivers/lenel/open_access/client.cr b/drivers/lenel/open_access/client.cr
new file mode 100644
index 00000000000..54a3e2b9f16
--- /dev/null
+++ b/drivers/lenel/open_access/client.cr
@@ -0,0 +1,166 @@
+require "http/client"
+require "http/params"
+require "responsible"
+require "uri"
+require "inactive-support/macro/args"
+
+require "./models"
+require "./error"
+
+# Lenel OpenAccess API wrapper.
+#
+# Provides thin abstractions over API endpoints. Requests are executed on the
+# pased transport. This can be a `PlaceOS::Driver`, `HTTP::Client` or other type
+# supporting the same set of base HTTP request methods.
+class Lenel::OpenAccess::Client
+ private getter transport : HTTP::Client
+
+ property app_id : String
+
+ property token : String?
+
+ def initialize(@transport, @app_id)
+ transport.before_request do |req|
+ req.headers["Application-Id"] = app_id
+ req.headers["Content-Type"] = "application/json"
+ req.headers["Session-Token"] = token.not_nil! unless token.nil?
+ end
+ end
+
+ Responsible.on_server_error do |response|
+ raise OpenAccess::Error.from_response response
+ end
+
+ Responsible.on_client_error do |response|
+ raise OpenAccess::Error.from_response response
+ end
+
+ # Gets the version of the attached OnGuard system.
+ def version
+ ~transport.get(
+ path: "/version?version=1.0",
+ ) >> NamedTuple(
+ product_name: String,
+ product_version: String,
+ )
+ end
+
+ # Enumerates the directories available for auth.
+ def directories
+ (~transport.get(
+ path: "/directories?version=1.0"
+ ) >> NamedTuple(
+ total_items: Int32,
+ item_list: Array({property_value_map: {ID: String, Name: String, directory_type: Int32}}),
+ ))[:item_list].map { |item| item[:property_value_map] }
+ end
+
+ # Creates a new auth session.
+ def login(
+ username user_name : String,
+ password : String,
+ directory_id : String?
+ )
+ ~transport.post(
+ path: "/authentication?version=1.0",
+ body: args.to_h.compact.to_json,
+ ) >> NamedTuple(
+ session_token: String,
+ token_expiration_time: Time,
+ )
+ end
+
+ # Removes an auth session.
+ def logout : Nil
+ ~transport.delete(
+ path: "/authentication?version=1.0",
+ )
+ end
+
+ # Request a connection keepalive to prevent session timeout.
+ def keepalive : Nil
+ ~transport.get(
+ path: "/keepalive?version=1.0",
+ )
+ end
+
+ # Creates a new instance of *entity*.
+ #
+ # API create responses return a partial object, which is provided here as an
+ # untyped return. This includes the object's database key (which varies
+ # between object types - ID, BADGEKEY etc), however contents of this is
+ # unspecified. The partial object is provided here, in full, with keys
+ # transformed to match how they appear in a type-safe model.
+ def create(entity : T.class, **props) forall T
+ ~transport.post(
+ path: "/instances?version=1.0",
+ body: {
+ type_name: T.type_name,
+ property_value_map: T.partial(**props),
+ }.to_json
+ ) >> Models::Untyped
+ end
+
+ # Retrieves instances of a particular *entity*.
+ #
+ # The search criteria specified in *filter* is a subset of SQL. This supports
+ # operations such as as:
+ # + exclusion `LastName != "Lake"`
+ # + wildcards `LastName like "La%"`
+ # + boolean operators `LastName = "Lake" OR FirstName = "Lisa"`
+ def lookup(
+ entity type_name : T.class,
+ filter : String? = nil,
+ page_number : Int32? = nil,
+ page_size : Int32? = nil,
+ order_by : String? = nil
+ ) : Array(T) forall T
+ params = HTTP::Params.new
+ args.merge(type_name: T.type_name).each do |key, val|
+ params.add key.to_s, val unless val.nil?
+ end
+ (~transport.get(
+ path: "/instances?version=1.0{params}",
+ ) >> NamedTuple(
+ page_number: Int32?,
+ page_size: Int32?,
+ total_pages: Int32,
+ total_items: Int32,
+ count: Int32,
+ item_list: Array(T),
+ ))[:item_list]
+ end
+
+ # Counts the number of instances of *entity*.
+ #
+ # *filter* may optionally be used to specify a subset of these.
+ def count(entity type_name : T.class, filter : String? = nil) forall T
+ params = HTTP::Params.encode args.merge type_name: T.type_name
+ (~transport.get(
+ path: "/count?version=1.0{params}"
+ ) >> NamedTuple(total_items: Int32))[:total_items]
+ end
+
+ # Updates a record of *entity*. Passed properties must include the types key and
+ # any fields to update.
+ def update(entity : T.class, **props) : T forall T
+ ~transport.put(
+ path: "/instances?version=1.0",
+ body: {
+ type_name: T.type_name,
+ property_value_map: T.partial(**props),
+ }.to_json
+ ) >> T
+ end
+
+ # Deletes an instance of *entity*.
+ def delete(entity : T.class, **props) : Nil forall T
+ ~transport.delete(
+ path: "/instances?version=1.0",
+ body: {
+ type_name: T.type_name,
+ property_value_map: T.partial(**props),
+ }.to_json
+ )
+ end
+end
diff --git a/drivers/lenel/open_access/error.cr b/drivers/lenel/open_access/error.cr
new file mode 100644
index 00000000000..aaa8d5d8adc
--- /dev/null
+++ b/drivers/lenel/open_access/error.cr
@@ -0,0 +1,24 @@
+require "json"
+
+class Lenel::OpenAccess::Error < Exception
+ alias Info = {error: {code: String, message: String?}}
+
+ def self.from_response(response)
+ # Although the API docs specify this is being in an "error" header, this
+ # appars as JSON within the response body when tested with OpenAccess 7.5
+ error = Error::Info.from_json response.body
+ new **error[:error]
+ rescue
+ new response.status.to_s
+ end
+
+ getter code
+
+ def initialize(@code : String, message : String? = nil)
+ if message
+ super "#{message} (#{code})"
+ else
+ super code
+ end
+ end
+end
diff --git a/drivers/lenel/open_access/models.cr b/drivers/lenel/open_access/models.cr
new file mode 100644
index 00000000000..974436465cb
--- /dev/null
+++ b/drivers/lenel/open_access/models.cr
@@ -0,0 +1,114 @@
+require "json"
+
+# DTO's for OpenAccess entities.
+#
+# These are intentionally lightweight. In cases where a entity holds a
+# relationship to another, these are _not_ auto-resolved. Original ID references
+# are kept in place. Types here a simply a thin wrapper for JSON serialization.
+module Lenel::OpenAccess::Models
+ PROPERTIES_KEY = "property_value_map"
+
+ # Base type for Lenel data objects.
+ abstract struct Element
+ include JSON::Serializable
+
+ # Name of the type as expected by the OpenAccess API endpoints.
+ def self.type_name
+ "Lnl_#{name.rpartition("::").last}"
+ end
+
+ # The Lenel API 'features' multiple case conventions, with varying
+ # consistency. It appears to be non-case sensitive for requests sent to it,
+ # however as response parsing _is_ more strict raw keys should come via first.
+ protected def normalise(key : String) : String
+ key.downcase
+ end
+
+ # Override the default JSON::Serializable behaviour to make keys case
+ # inensitive when deserialising.
+ def initialize(*, __pull_for_json_serializable pull : ::JSON::PullParser)
+ {% begin %}
+ {% properties = {} of Nil => Nil %}
+ {% for ivar in @type.instance_vars %}
+ {% ann = ivar.annotation(::JSON::Field) %}
+ {% unless ann && ann[:ignore] %}
+ {% properties[ivar.id] = ivar.type %}
+ %var{ivar.id} = nil
+ {% end %}
+ {% end %}
+
+ # All entities come wrapeed inside a standard key...
+ pull.on_key! PROPERTIES_KEY do
+
+ pull.read_begin_object
+ until pull.kind.end_object?
+ %key_location = pull.location
+ key = normalise pull.read_object_key
+ case key
+ {% for name, type in properties %}
+ when {{name.stringify}}
+ %var{name} = ::Union({{type}}).new pull
+ {% end %}
+ else
+ on_unknown_json_attribute(pull, key, %key_location)
+ end
+ end
+ pull.read_next
+
+ end
+
+ {% for name, type in properties %}
+ @{{name}} = %var{name}.as {{type}}
+ {% end %}
+ {% end %}
+ end
+
+ # Provide a compile-time check to ensure *properties* is a subset of *self*.
+ def self.partial(**properties : **T) : T forall T
+ {% for key in T.keys %}
+ {% raise %(no "#{key}" property on #{@type.name}) unless @type.has_method? key %}
+ {% end %}
+ properties
+ end
+ end
+
+ struct Untyped < Element
+ include JSON::Serializable::Unmapped
+ forward_missing_to json_unmapped
+ end
+
+ abstract struct Person < Element
+ getter id : Int32
+ getter firstname : String
+ getter lastname : String
+ end
+
+ struct Badge < Element
+ getter badgekey : Int32
+ getter activate : Time
+ getter deactivate : Time
+ getter id : Int64
+ getter personid : Int32
+ getter status : Int32
+ getter type : Int32
+ getter uselimit : Int32
+ end
+
+ struct BadgeType < Element
+ enum BadgeTypeClass
+ Standard
+ Temporary
+ Visitor
+ Guest
+ SpecialPurpose
+ end
+ getter id : Int32
+ getter name : String
+ getter badgetypeclass : BadgeTypeClass
+ getter usemobilecredential : Bool
+ end
+
+ struct Cardholder < Person
+ getter email : String
+ end
+end
diff --git a/drivers/lenel/open_access_spec.cr b/drivers/lenel/open_access_spec.cr
new file mode 100644
index 00000000000..db2938753c6
--- /dev/null
+++ b/drivers/lenel/open_access_spec.cr
@@ -0,0 +1,170 @@
+private macro respond_with(code, body)
+ res.headers["Content-Type"] = "application/json"
+ res.status_code = {{code}}
+ res.output << {{body}}.to_json
+end
+
+DriverSpecs.mock_driver "Lenel::OpenAccess" do
+ # Auth on connect
+ expect_http_request do |req, res|
+ req.method.should eq("POST")
+ req.path.should eq("/authentication")
+ respond_with 200, {
+ session_token: "abc123",
+ token_expiration_time: "#{(Time.utc + 2.weeks).to_rfc3339}",
+ }
+ end
+
+ # Re-auth on creds update
+ settings({
+ username: "foo",
+ password: "bar",
+ directory_id: "baz",
+ application_id: "",
+ })
+ expect_http_request do |req, res|
+ req.method.should eq("POST")
+ req.path.should eq("/authentication")
+ body = JSON.parse req.body.not_nil!
+ body["user_name"].should eq("foo")
+ body["password"].should eq("bar")
+ body["directory_id"].should eq("baz")
+ respond_with 200, {
+ session_token: "abc123",
+ token_expiration_time: "#{(Time.utc + 2.weeks).to_rfc3339}",
+ }
+ end
+
+ # Version lookup
+ version = exec(:version)
+ expect_http_request do |req, res|
+ req.method.should eq("GET")
+ req.path.should eq("/version")
+ req.headers["Session-Token"].should eq("abc123")
+ respond_with 200, {
+ product_name: "OnGuard 7.6",
+ product_version: "7.6.001",
+ version: "1.0",
+ }
+ end
+ version = version.get.not_nil!
+ version["product_version"].should eq("7.6.001")
+
+ # Error handling
+ failing_request = exec(:version)
+ expect_http_request do |req, res|
+ req.method.should eq("GET")
+ req.path.should eq("/version")
+ respond_with 401, {
+ error: {
+ code: "openaccess.general.invalidapplicationid",
+ message: "You are not licensed for OpenAccess.",
+ },
+ }
+ end
+ expect_raises(PlaceOS::Driver::RemoteException) do
+ failing_request.get
+ end
+
+ # Cardholder CRUD
+
+ example_cardholder = {
+ email: "sales@vandelayindustries.com",
+ firstname: "Kel",
+ lastname: "Varnsen",
+ }
+
+ created_cardholder = exec(:create_cardholder, **example_cardholder)
+ expect_http_request do |req, res|
+ req.method.should eq("GET")
+ req.path.should eq("/count")
+ req.query_params["type_name"]?.should eq("Lnl_Cardholder")
+ req.query_params["filter"]?.should eq(%(email = "sales@vandelayindustries.com"))
+ respond_with 200, {total_items: 0}
+ end
+ expect_http_request do |req, res|
+ req.method.should eq("POST")
+ req.path.should eq("/instances")
+ body = JSON.parse req.body.not_nil!
+ body["type_name"]?.should eq("Lnl_Cardholder")
+ body["property_value_map"]?.try do |prop|
+ prop["email"].should eq("sales@vandelayindustries.com")
+ prop["firstname"].should eq("Kel")
+ prop["lastname"].should eq("Varnsen")
+ end
+ respond_with 200, {
+ type_name: "Lnl_Cardholder",
+ property_value_map: {
+ ID: 1,
+ },
+ }
+ end
+ created_cardholder = created_cardholder.get.not_nil!
+ created_cardholder["id"]?.should eq(1)
+
+ queried_cardholder = exec(:lookup_cardholder, email: "sales@vandelayindustries.com")
+ expect_http_request do |req, res|
+ req.method.should eq("GET")
+ req.path.should eq("/instances")
+ req.query_params["type_name"]?.should eq("Lnl_Cardholder")
+ req.query_params["filter"]?.should eq(%(email = "sales@vandelayindustries.com"))
+ respond_with 200, {
+ total_pages: 1,
+ total_items: 1,
+ count: 1,
+ type_name: "Lnl_Cardholder",
+ item_list: [{
+ property_value_map: {
+ ID: 1,
+ EMAIL: "sales@vandelyindustries.com",
+ FIRSTNAME: "Kel",
+ LASTNAME: "Varnsen",
+ },
+ }],
+ }
+ end
+ queried_cardholder = queried_cardholder.get.not_nil!
+ queried_cardholder["id"]?.should eq(1)
+ queried_cardholder["firstname"]?.should eq("Kel")
+
+ exec(:delete_cardholder, id: 1)
+ expect_http_request do |req, res|
+ req.method.should eq("DELETE")
+ req.path.should eq("/instances")
+ body = JSON.parse req.body.not_nil!
+ body["type_name"]?.should eq("Lnl_Cardholder")
+ body.dig("property_value_map", "id").should eq(1)
+ res.status_code = 200
+ end
+
+ created_badge = exec(:create_badge, type: 5, personid: 1, id: 123)
+ expect_http_request do |req, res|
+ req.method.should eq("POST")
+ req.path.should eq("/instances")
+ body = JSON.parse req.body.not_nil!
+ body["type_name"]?.should eq("Lnl_Badge")
+ body["property_value_map"]?.try do |prop|
+ prop["type"].should eq(5)
+ prop["personid"].should eq(1)
+ prop["id"].should eq(123)
+ end
+ respond_with 200, {
+ type_name: "Lnl_Badge",
+ property_value_map: {
+ BADGEKEY: 1,
+ },
+ }
+ end
+ created_badge = created_badge.get.not_nil!
+ created_badge["badgekey"]?.should eq(1)
+
+ exec(:delete_badge, badgekey: 1)
+ expect_http_request do |req, res|
+ req.method.should eq("DELETE")
+ req.path.should eq("/instances")
+ body = JSON.parse req.body.not_nil!
+ body["type_name"]?.should eq("Lnl_Badge")
+ body.dig("property_value_map", "badgekey").should eq(1)
+ res.status_code = 200
+ end
+end
diff --git a/drivers/lg/displays/ls5.cr b/drivers/lg/displays/ls5.cr
new file mode 100644
index 00000000000..fd17b4e8ff4
--- /dev/null
+++ b/drivers/lg/displays/ls5.cr
@@ -0,0 +1,301 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+class Lg::Displays::Ls5 < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Input
+ Dvi = 0x70
+ Hdmi = 0xA0
+ HdmiDtv = 0x90
+ Hdmi2 = 0xA1
+ Hdmi2Dtv = 0x91
+ DisplayPort = 0xD0
+ DisplayPortDtv = 0xC0
+ end
+
+ include Interface::InputSelection(Input)
+
+ # Discovery Information
+ tcp_port 9761
+ descriptive_name "LG WebOS LCD Monitor"
+ generic_name :Display
+ # This device does not hold the connection open. Must be configured as makebreak
+ makebreak!
+
+ default_settings({
+ rs232_control: false,
+ display_id: 1,
+ })
+
+ @display_id : Int32 = 0
+ @id_num : Int32 = 1
+ @rs232 : Bool = false
+ @id : String = ""
+ @last_broadcast : String? = nil
+ @connected : Bool = false
+
+ DELIMITER = 0x78_u8 # 'x'
+
+ def on_load
+ # Communication settings
+ queue.delay = 150.milliseconds
+ transport.tokenizer = Tokenizer.new(Bytes[DELIMITER])
+ on_update
+ end
+
+ def on_update
+ @rs232 = setting(Bool, :rs232_control)
+ @id_num = setting(Int32, :display_id)
+ @id = @id_num.to_s.rjust(2, '0')
+ end
+
+ def connected
+ @connected = true
+ self[:connected] = true
+ wake_on_lan
+ no_signal_off
+ auto_off
+ local_button_lock
+ pm_mode
+ schedule.every(50.seconds, true) do
+ do_poll
+ end
+ end
+
+ def disconnected
+ @connected = false
+ self[:connected] = false
+ schedule.clear
+ end
+
+ enum Command
+ Power = 0x61 # 'a'
+ Input = 0x62 # 'b'
+ AspectRatio = 0x63 # 'c'
+ ScreenMute = 0x64 # 'd'
+ VolumeMute = 0x65 # 'e'
+ Volume = 0x66 # 'f'
+ Contrast = 0x67 # 'g'
+ Brightness = 0x68 # 'h'
+ Sharpness = 0x6B # 'k'
+ AutoOff = 0x6E # 'n'
+ LocalButtonLock = 0x6F # 'o'
+ WakeOnLan = 0x77 # 'w'
+ NoSignalOff = 0x67 # 'g'
+ PmMode = 0x6E # 'n'
+ end
+ {% for name in Command.constants %}
+ @[Security(Level::Administrator)]
+ def {{name.id.underscore}}?(priority : Int32 = 0)
+ do_send(Command::{{name.id}}, 0xFF, priority: priority, name: {{name.id.underscore.stringify}} + "_status")
+ end
+ {% end %}
+
+ def power(state : Bool, broadcast : String? = nil)
+ if state
+ if @rs232
+ do_send(Command::Power, 1, name: "power", priority: 99)
+ else
+ wake(broadcast || @last_broadcast)
+ end
+ end
+ # To power on, unmute the display
+ # To power off, mute the display
+ mute(!state) if @connected
+ end
+
+ def hard_off
+ do_send(Command::Power, 0, name: "power", priority: 99, clear_queue: true)
+ end
+
+ def switch_to(input : Input, **options)
+ do_send(Command::Input, input.value, 'x', name: "input", delay: 2.seconds)
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ if layer.video? || layer.audio_video?
+ do_send(Command::ScreenMute, state ? 1 : 0, name: "mute_video")
+ end
+
+ if (layer.audio? || layer.audio_video?) && (self[:audio_mute]?.try &.as_bool) != state
+ do_send(Command::VolumeMute, state ? 0 : 1, name: "mute_audio")
+ end
+
+ state
+ end
+
+ enum Ratio
+ Square = 0x01
+ Wide = 0x02
+ Zoom = 0x04
+ Scan = 0x09
+ Program = 0x06
+ end
+
+ def aspect_ratio(ratio : Ratio)
+ do_send(Command::AspectRatio, ratio.value, name: "aspect_ratio", delay: 1.second)
+ end
+
+ def do_poll
+ if @rs232
+ power?
+ if self[:hard_power]?.try &.as_bool
+ screen_mute?
+ input?
+ volume_mute?
+ volume?
+ end
+ elsif @connected
+ screen_mute?
+
+ if @id_num == 1
+ input?
+ volume_mute?
+ volume?
+ end
+ elsif self[:power_target]?.try &.as_bool
+ power(true)
+ end
+ end
+
+ def input?(priority : Int32 = 0)
+ do_send(Command::Input, 0xFF, 'x', priority: priority)
+ end
+
+ {% for name in ["Volume", "Contrast", "Brightness", "Sharpness"] %}
+ @[Security(Level::Administrator)]
+ def {{name.id.downcase}}(value : Int32)
+ val = value.clamp(0, 100)
+ do_send(Command::{{name.id}}, val, name: {{name.id.downcase.stringify}})
+ end
+ {% end %}
+
+ # This is only necessary for Command::PmMode and Command::NoSignalOff
+ # Both the responses for contrast/no_signal_off will have data[0] == 'g'
+ # Same thing for auto_off/pm_mode with data[0] == 'n'
+ # We will use the send and callback method to ensure these responses are processed properly
+ private def process_response(data, task)
+ if (resp_value = get_response_value(data)) == -1
+ task.abort
+ else
+ self[task.name] = task.name == "pm_mode" ? resp_value : resp_value == 1
+ task.success
+ end
+ end
+
+ def pm_mode(mode : Int32 = 3)
+ command = build_command(Command::PmMode, mode, 's')
+ send(command, name: "pm_mode") { |data, task| process_response(data, task) }
+ end
+
+ def no_signal_off(state : Bool = false)
+ val = state ? 1 : 0
+ command = build_command(Command::NoSignalOff, val, 'f')
+ send(command, name: "no_signal_off") { |data, task| process_response(data, task) }
+ end
+
+ # 0 = Off, 1 = lock all except Power buttons, 2 = lock all buttons. Default to 2 as power off from local button results in network offline
+ def local_button_lock(state : Bool = true)
+ val = state ? 2 : 0
+ do_send(Command::LocalButtonLock, val, 't', name: "local_button_lock")
+ end
+
+ def auto_off(state : Bool = false)
+ val = state ? 1 : 0
+ do_send(Command::AutoOff, val, 'm', name: "disable_auto_off")
+ end
+
+ def wake_on_lan(state : Bool = true)
+ val = state ? 1 : 0
+ do_send(Command::WakeOnLan, val, 'f', name: "enable_wake_on_lan")
+ end
+
+ def wake(broadcast : String? = nil)
+ if mac = setting?(String, :mac_address)
+ # config is the database model representing this device
+ wake_device(mac, broadcast)
+ logger.debug {
+ info = "Wake on Lan for MAC #{mac}"
+ if b = broadcast
+ info += " directed to VLAN #{b}"
+ end
+ info
+ }
+ else
+ logger.warn { "No MAC address provided" }
+ end
+ end
+
+ private def get_response_value(response : Bytes)
+ logger.debug { "LG sent #{response}" }
+ resp = String.new(response).split(' ').last
+ # Default to -1 which means an error
+ resp_value = -1
+ if resp[0..1] == "OK" # Extract the response value
+ # Special case for PM Mode
+ if resp[2..3] == "0c"
+ resp_value = resp[4..-2].to_i(16)
+ else
+ resp_value = resp[2..-2].to_i(16)
+ end
+ end
+ resp_value
+ end
+
+ def received(data, task)
+ return task.try &.abort if (resp_value = get_response_value(data)) == -1
+ command = Command.from_value(data[0])
+ logger.debug { "Received command #{command}" }
+
+ case command
+ when .power?
+ self[:hard_power] = resp_value == 1
+ self[:power] = false unless self[:hard_power].as_bool
+ when .input?
+ self[:input] = Input.from_value(resp_value)
+ when .aspect_ratio?
+ self[:aspect_ratio] = Ratio.from_value(resp_value)
+ when .screen_mute?
+ self[:power] = resp_value == 0
+ when .volume_mute?
+ self[:audio_mute] = resp_value == 0
+ when .contrast?, .brightness?, .sharpness?, .volume?
+ self[command.to_s.underscore] = resp_value
+ when .wake_on_lan?, .auto_off?
+ self[command.to_s.underscore] = resp_value == 1
+ when .local_button_lock?
+ self[:local_button_lock] = resp_value == 2
+ else
+ return task.try &.retry
+ end
+
+ task.try &.success
+ end
+
+ # From manual
+ # [Command1]: identifies between the factory setting and the user setting modes.
+ # Default c1 to 'k' which appears to be for user settings
+ # and which most commands use (e.g. Mute, Screen off, Volume, Brightness)
+ # Note: this is not a Command instance method as this needs access to @id
+ private def build_command(command : Command, data : Int, c1 : Char = 'k')
+ # Command::PmMode and Command::AutoOff both are equal to 0x6E == 'n'
+ # However, PmMode has c1 == 's' while AutoOff has c1 == 'm'
+ # So this is how we can differentiate whether the command we want to send is PmMode
+ if command.pm_mode? && c1 == 's'
+ "#{c1}#{command.value.chr} #{@id} 0c #{data.to_s(16, upcase: true).rjust(2, '0')}\r"
+ else
+ "#{c1}#{command.value.chr} #{@id} #{data.to_s(16, upcase: true).rjust(2, '0')}\r"
+ end
+ end
+
+ private def do_send(command : Command, data : Int, c1 : Char = 'k', **options)
+ send(build_command(command, data, c1), **options)
+ end
+end
diff --git a/drivers/lg/displays/ls5_spec.cr b/drivers/lg/displays/ls5_spec.cr
new file mode 100644
index 00000000000..6fc5141485c
--- /dev/null
+++ b/drivers/lg/displays/ls5_spec.cr
@@ -0,0 +1,70 @@
+DriverSpecs.mock_driver "Lg::Displays::Ls5" do
+ # Execute a command (triggers the connection)
+ exec(:power?)
+ expect_reconnect
+
+ # connected
+ # wake_on_lan(true)
+ should_send("fw 01 01\r")
+ responds("w 01 OK01x")
+ status[:wake_on_lan].should eq(true)
+ # no_signal_off(false)
+ should_send("fg 01 00\r")
+ responds("g 01 OK00x")
+ status[:no_signal_off].should eq(false)
+ # auto_off(false)
+ should_send("mn 01 00\r")
+ responds("n 01 OK00x")
+ status[:auto_off].should eq(false)
+ # local_button_lock(true)
+ should_send("to 01 02\r")
+ responds("o 01 OK02x")
+ status[:local_button_lock].should eq(true)
+ # pm_mode(3)
+ should_send("sn 01 0c 03\r")
+ responds("n 01 OK0c03x")
+ status[:pm_mode].should eq(3)
+ # do_poll && self[:connected] == true && @id_num == 1
+ # screen_mute?
+ should_send("kd 01 FF\r")
+ responds("d 01 OK01x")
+ status[:power].should eq(false)
+ # input?
+ should_send("xb 01 FF\r")
+ responds("b 01 OKA0x")
+ status[:input].should eq("Hdmi")
+ # volume_mute?
+ should_send("ke 01 FF\r")
+ responds("e 01 OK00x")
+ status[:audio_mute].should eq(true)
+ # volume?
+ should_send("kf 01 FF\r")
+ responds("f 01 OK08x")
+ status[:volume].should eq(8)
+
+ exec(:switch_to, "dvi")
+ should_send("xb 01 70\r")
+ responds("b 01 OK70x")
+ status[:input].should eq("Dvi")
+
+ exec(:power, true)
+ sleep 2 # since switch_to has 2 seconds of delay
+ # mute_video(false)
+ should_send("kd 01 00\r")
+ responds("d 01 OK00x")
+ status[:power].should eq(true)
+ # mute_audio(false)
+ should_send("ke 01 01\r")
+ responds("e 01 OK01x")
+ status[:audio_mute].should eq(false)
+
+ exec(:power, false)
+ # mute_video(true)
+ should_send("kd 01 01\r")
+ responds("d 01 OK01x")
+ status[:power].should eq(false)
+ # mute_audio(true)
+ should_send("ke 01 00\r")
+ responds("e 01 OK00x")
+ status[:audio_mute].should eq(true)
+end
diff --git a/drivers/lumens/dc193.cr b/drivers/lumens/dc193.cr
new file mode 100644
index 00000000000..cb6005fa976
--- /dev/null
+++ b/drivers/lumens/dc193.cr
@@ -0,0 +1,245 @@
+module Lumens; end
+
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/zoomable"
+
+# Documentation: https://aca.im/driver_docs/Lumens/DC193-Protocol.pdf
+# RS232 controlled device
+
+class Lumens::DC193 < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Zoomable
+
+ # Discovery Information
+ descriptive_name "Lumens DC 193 Document Camera"
+ generic_name :DocCam
+
+ # Global Cache Port
+ tcp_port 4999
+
+ def on_load
+ # Communication settings
+ queue.delay = 100.milliseconds
+ transport.tokenizer = Tokenizer.new(6)
+
+ # Ensure range is roughly accurate
+ @zoom_range = 0..@zoom_max
+ end
+
+ def connected
+ schedule.every(50.seconds) { query_status }
+ query_status
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def query_status
+ # Responses are JSON encoded
+ if power?.get == "true"
+ lamp?
+ zoom?
+ frozen?
+ max_zoom?
+ picture_mode?
+ end
+ end
+
+ def power(state : Bool)
+ state = state ? 0x01_u8 : 0x00_u8
+ send Bytes[0xA0, 0xB0, state, 0x00, 0x00, 0xAF], name: :power
+ power?
+ end
+
+ def power?
+ # item 58 call system status
+ send Bytes[0xA0, 0xB7, 0x00, 0x00, 0x00, 0xAF], priority: 0
+ end
+
+ def lamp(state : Bool, head_led : Bool = false)
+ return false if @frozen
+
+ lamps = if state && head_led
+ 1_u8
+ elsif state
+ 2_u8
+ elsif head_led
+ 3_u8
+ else
+ 0_u8
+ end
+
+ send Bytes[0xA0, 0xC1, lamps, 0x00, 0x00, 0xAF], name: :lamp
+ end
+
+ def lamp?
+ send Bytes[0xA0, 0x50, 0x00, 0x00, 0x00, 0xAF], priority: 0
+ end
+
+ def zoom_to(position : Int32, auto_focus : Bool = true, index : Int32 | String = 0)
+ return false if @frozen
+
+ position = (position < 0 ? 0 : @zoom_max) unless @zoom_range.includes?(position)
+ low = (position & 0xFF).to_u8
+ high = ((position >> 8) & 0xFF).to_u8
+ auto_focus = auto_focus ? 0x1F_u8 : 0x13_u8
+ send Bytes[0xA0, auto_focus, low, high, 0x00, 0xAF], name: :zoom_to
+ end
+
+ def zoom(direction : ZoomDirection, index : Int32 | String = 1)
+ return false if @frozen
+
+ case direction
+ when ZoomDirection::Stop
+ send Bytes[0xA0, 0x10, 0x00, 0x00, 0x00, 0xAF]
+ # Ensures this request is at the normal priority and ordering is preserved
+ zoom?(priority: queue.priority)
+ # This prevents the auto-focus if someone starts zooming again
+ auto_focus(name: "zoom")
+ when ZoomDirection::In
+ send Bytes[0xA0, 0x11, 0x00, 0x00, 0x00, 0xAF], name: :zoom
+ when ZoomDirection::Out
+ send Bytes[0xA0, 0x11, 0x01, 0x00, 0x00, 0xAF], name: :zoom
+ end
+ end
+
+ def auto_focus(name : String = "auto_focus")
+ return false if @frozen
+
+ send Bytes[0xA0, 0xA3, 0x01, 0x00, 0x00, 0xAF], name: name
+ end
+
+ def zoom?(priority : Int32 = 0)
+ send Bytes[0xA0, 0x60, 0x00, 0x00, 0x00, 0xAF], priority: priority
+ end
+
+ def freeze(state : Bool)
+ state = state ? 1_u8 : 0_u8
+ send Bytes[0xA0, 0x2C, state, 0x00, 0x00, 0xAF], name: :freeze
+ end
+
+ def frozen?
+ send Bytes[0xA0, 0x78, 0x00, 0x00, 0x00, 0xAF], priority: 0
+ end
+
+ def picture_mode(state : String)
+ return false if @frozen
+
+ mode = case state.downcase
+ when "photo"
+ 0x00_u8
+ when "text"
+ 0x01_u8
+ when "greyscale", "grayscale"
+ 0x02_u8
+ else
+ raise ArgumentError.new("unknown picture mode #{state}")
+ end
+ send Bytes[0xA0, 0xA7, mode, 0x00, 0x00, 0xAF], name: :picture_mode
+ end
+
+ def picture_mode?
+ send Bytes[0xA0, 0x51, 0x00, 0x00, 0x00, 0xAF], priority: 0
+ end
+
+ def max_zoom?
+ send Bytes[0xA0, 0x8A, 0x00, 0x00, 0x00, 0xAF], priority: 0
+ end
+
+ @[Flags]
+ enum Status
+ Error
+ Ignored
+ Reserved1
+ Reserved2
+ Focusing
+ Zooming
+ Iris
+ Reserved3
+ end
+
+ COMMANDS = {
+ 0xC1_u8 => :lamp,
+ 0xB0_u8 => :power,
+ 0xB7_u8 => :power_staus,
+ 0xA7_u8 => :picture_mode,
+ 0xA3_u8 => :auto_focus,
+ 0x8A_u8 => :max_zoom,
+ 0x78_u8 => :frozen_status,
+ 0x60_u8 => :zoom_staus,
+ 0x51_u8 => :picture_mode_staus,
+ 0x50_u8 => :lamp_staus,
+ 0x2C_u8 => :freeze,
+ 0x1F_u8 => :zoom_direct_auto_focus,
+ 0x13_u8 => :zoom_direct,
+ 0x11_u8 => :zoom,
+ 0x10_u8 => :zoom_stop,
+ }
+
+ @ready : Bool = true
+ @power : Bool = false
+ @zoom_max : Int32 = 864
+ @lamp : Bool = false
+ @head_led : Bool = false
+ @frozen : Bool = false
+
+ PICTURE_MODES = {:photo, :test, :greyscale}
+
+ def received(data, task)
+ logger.debug { "Lumens sent: #{data.hexstring}" }
+
+ status = Status.from_value(data[4].to_i)
+ self[:zooming] = status.zooming?
+ self[:focusing] = status.focusing?
+ self[:iris_adjusting] = status.iris?
+
+ return task.try &.abort("bad request") if status.error?
+ return task.try &.retry("device busy") if status.ignored?
+
+ result = case COMMANDS[data[1]]?
+ when :power
+ data[2] == 0x01_u8
+ when :power_staus
+ @ready = data[2] == 0x01_u8
+ @power = data[3] == 0x01_u8
+ logger.debug { "System power: #{@power}, ready: #{@ready}" }
+ self[:ready] = @ready
+ self[:power] = @power
+ when :max_zoom
+ @zoom_max = data[2].to_i + (data[3].to_i << 8)
+ @zoom_range = 0..@zoom_max
+ self[:zoom_range] = {min: 0, max: @zoom_max}
+ when :frozen_status, :freeze
+ self[:frozen] = @frozen = data[2] == 1_u8
+ when :zoom_staus, :zoom_direct_auto_focus, :zoom_direct
+ @zoom = data[2].to_i + (data[3].to_i << 8)
+ self[:zoom] = @zoom
+ when :picture_mode_staus, :picture_mode
+ self[:picture_mode] = PICTURE_MODES[data[2].to_i]
+ when :lamp_staus, :lamp
+ case data[2]
+ when 0_u8
+ @head_led = @lamp = false
+ when 1_u8
+ @head_led = @lamp = true
+ when 2_u8
+ @head_led = false
+ @lamp = true
+ when 3_u8
+ @head_led = true
+ @lamp = false
+ end
+ self[:head_led] = @head_led
+ self[:lamp] = @lamp
+ when :auto_focus
+ # Can ignore this response
+ else
+ error = "Unknown command #{data[1]}"
+ logger.debug { error }
+ return task.try &.abort(error)
+ end
+
+ task.try &.success(result)
+ end
+end
diff --git a/drivers/lumens/dc193_spec.cr b/drivers/lumens/dc193_spec.cr
new file mode 100644
index 00000000000..8ec70b4692d
--- /dev/null
+++ b/drivers/lumens/dc193_spec.cr
@@ -0,0 +1,8 @@
+DriverSpecs.mock_driver "Lumens::DC193" do
+ # On connect it queries the state of the device
+ should_send(Bytes[0xA0, 0xB7, 0x00, 0x00, 0x00, 0xAF])
+ transmit(Bytes[0xA0, 0xB7, 0x01, 0x00, 0x00, 0xAF])
+
+ status[:ready].should be_true
+ status[:power].should be_false
+end
diff --git a/drivers/lutron/lighting.cr b/drivers/lutron/lighting.cr
new file mode 100644
index 00000000000..fd06cccd15a
--- /dev/null
+++ b/drivers/lutron/lighting.cr
@@ -0,0 +1,219 @@
+module Lutron; end
+
+# Documentation: https://aca.im/driver_docs/Lutron/lutron-lighting.pdf
+
+# Device defaults
+# Login #1: nwk
+# Login #2: nwk2
+
+# Login: lutron
+# Password: integration
+
+class Lutron::Lighting < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 23
+ descriptive_name "Lutron Lighting Gateway"
+ generic_name :Lighting
+
+ def on_load
+ # Communication settings
+ queue.wait = false
+ queue.delay = 100.milliseconds
+ transport.tokenizer = Tokenizer.new("\r\n")
+
+ on_update
+ end
+
+ @trigger_type : String = "area"
+ @login : String = "nwk"
+
+ def on_update
+ @login = setting?(String, :login) || "nwk"
+ @trigger_type = setting?(String, :trigger) || "area"
+ end
+
+ def connected
+ send "#{@login}\r\n", priority: 9999
+
+ schedule.every(40.seconds) do
+ logger.debug { "-- Polling Lutron" }
+ scene? 1
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def restart
+ send_cmd "RESET", 0
+ end
+
+ # on or off
+ def lighting(device : Int32, state : Bool, action : Int32 = 1)
+ level = state ? 100 : 0
+ light_level(device, level)
+ end
+
+ # ===============
+ # OUTPUT COMMANDS
+ # ===============
+
+ # dimmers, CCOs, or other devices in a system that have a controllable output
+ def level(
+ device : Int32,
+ level : Int32,
+ rate : Int32 = 1000,
+ component : String = "output"
+ )
+ level = level.clamp(0, 100)
+ seconds = rate / 1000
+ min = seconds / 60
+ seconds -= min * 60
+ time = "#{min.to_s.rjust(2, '0')}:#{seconds.to_s.rjust(2, '0')}"
+ send_cmd component.upcase, device, 1, level, time
+ end
+
+ def blinds(device : String, action : String, component : String = "shadegrp")
+ case action.downcase
+ when "raise", "up"
+ send_cmd component.upcase, device, 3
+ when "lower", "down"
+ send_cmd component.upcase, device, 2
+ when "stop"
+ send_cmd component.upcase, device, 4
+ end
+ end
+
+ # =============
+ # AREA COMMANDS
+ # =============
+ def scene(area : Int32, scene : Int32, component : String = "area")
+ send_cmd(component.upcase, area, 6, scene).get
+ scene?(area, component)
+ end
+
+ def scene?(area : Int32, component : String = "area")
+ send_query component.upcase, area, 6
+ end
+
+ def occupancy?(area : Int32)
+ send_query "AREA", area, 8
+ end
+
+ def daylight_mode?(area : Int32)
+ send_query "AREA", area, 7
+ end
+
+ def daylight(area : Int32, mode : Bool)
+ val = mode ? 1 : 2
+ send_cmd "AREA", area, 7, val
+ end
+
+ # ===============
+ # DEVICE COMMANDS
+ # ===============
+ def button_press(area : Int32, button : Int32)
+ send_cmd "DEVICE", area, button, 3
+ end
+
+ def led(area : Int32, device : Int32, state : Int32 | Bool)
+ val = if state.is_a?(Int32)
+ state
+ else
+ state ? 1 : 0
+ end
+
+ send_cmd "DEVICE", area, device, 9, val
+ end
+
+ def led?(area : Int32, device : Int32)
+ send_query "DEVICE", area, device, 9
+ end
+
+ # =============
+ # COMPATIBILITY
+ # =============
+ def trigger(area : Int32, scene : Int32)
+ scene(area, scene, @trigger_type)
+ end
+
+ def light_level(area : Int32, level : Int32, component : String? = nil, fade : Int32 = 1000)
+ if component
+ level(area, level, fade, component)
+ else
+ level(area, level, fade, "area")
+ end
+ end
+
+ Errors = {
+ "1" => "Parameter count mismatch",
+ "2" => "Object does not exist",
+ "3" => "Invalid action number",
+ "4" => "Parameter data out of range",
+ "5" => "Parameter data malformed",
+ "6" => "Unsupported Command",
+ }
+
+ Occupancy = {
+ "1" => "unknown",
+ "2" => "inactive",
+ "3" => "occupied",
+ "4" => "unoccupied",
+ }
+
+ def received(data, task)
+ data = String.new(data)
+ logger.debug { "Lutron sent: #{data}" }
+
+ parts = data.split(",")
+ component = parts[0][1..-1].downcase
+
+ case component
+ when "area", "output", "shadegrp"
+ area = parts[1]
+ action = parts[2].to_i
+ param = parts[3]
+
+ case action
+ when 1 # level
+ self["#{component}#{area}_level"] = param.to_f
+ when 6 # Scene
+ self["#{component}#{area}"] = param.to_i
+ when 7
+ self["#{component}#{area}_daylight"] = param == "1"
+ when 8
+ self["#{component}#{area}_occupied"] = Occupancy[param]
+ end
+ when "device"
+ area = parts[1]
+ device = parts[2]
+ action = parts[3].to_i
+
+ case action
+ when 7 # Scene
+ self["device#{area}_#{device}"] = parts[4].to_i
+ when 9 # LED state
+ self["device#{area}_#{device}_led"] = parts[4].to_i
+ end
+ when "error"
+ error = "error #{parts[1]}: #{Errors[parts[1]]}"
+ logger.warn { error }
+ return task.try &.abort(error)
+ end
+
+ task.try &.success
+ end
+
+ protected def send_cmd(*command)
+ cmd = "##{command.join(",")}"
+ logger.debug { "Requesting: #{cmd}" }
+ send("#{cmd}\r\n")
+ end
+
+ protected def send_query(*command)
+ cmd = "?#{command.join(",")}"
+ logger.debug { "Querying: #{cmd}" }
+ send("#{cmd}\r\n")
+ end
+end
diff --git a/drivers/lutron/lighting_spec.cr b/drivers/lutron/lighting_spec.cr
new file mode 100644
index 00000000000..c7a6b30945b
--- /dev/null
+++ b/drivers/lutron/lighting_spec.cr
@@ -0,0 +1,34 @@
+DriverSpecs.mock_driver "Lutron::Lighting" do
+ # Module waits for this text to become ready
+ transmit "login: "
+ should_send "nwk\r\n"
+ transmit "connection established\r\n"
+
+ # Perform actions
+ response = exec(:scene?, area: 1)
+ should_send("?AREA,1,6\r\n")
+ responds("~AREA,1,6,2\r\n")
+ response.get
+ status[:area1].should eq(2)
+
+ transmit "~DEVICE,1,6,9,1\r\n"
+ status[:device1_6_led].should eq(1)
+
+ transmit "~AREA,1,6,1\r\n"
+ status[:area1].should eq(1)
+
+ transmit "~OUTPUT,53,1,100.00\r\n"
+ status[:output53_level].should eq(100.00)
+
+ transmit "~SHADEGRP,26,1,100.00\r\n"
+ status[:shadegrp26_level].should eq(100.00)
+
+ exec(:scene, area: 1, scene: 3)
+ should_send("#AREA,1,6,3\r\n")
+ responds("\r\n")
+
+ should_send("?AREA,1,6\r\n")
+ transmit "~AREA,1,6,3\r\n"
+
+ status[:area1].should eq(3)
+end
diff --git a/drivers/message_media/sms.cr b/drivers/message_media/sms.cr
new file mode 100644
index 00000000000..f593f310a47
--- /dev/null
+++ b/drivers/message_media/sms.cr
@@ -0,0 +1,62 @@
+module MessageMedia; end
+
+# Documentation: https://developers.messagemedia.com/code/messages-api-documentation/
+require "placeos-driver/interface/sms"
+
+class MessageMedia::SMS < PlaceOS::Driver
+ include Interface::SMS
+
+ # Discovery Information
+ generic_name :SMS
+ descriptive_name "MessageMedia SMS service"
+ uri_base "https://api.messagemedia.com"
+
+ default_settings({
+ basic_auth: {
+ username: "srvc_acct",
+ password: "password!",
+ },
+ })
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ end
+
+ def send_sms(
+ phone_numbers : String | Array(String),
+ message : String,
+ format : String? = "SMS",
+ source : String? = nil
+ )
+ phone_numbers = [phone_numbers] unless phone_numbers.is_a?(Array)
+
+ # Could be MMS etc
+ format = format || "SMS"
+
+ numbers = phone_numbers.map do |number|
+ payload = {
+ :content => message,
+ :destination_number => number,
+ :format => format,
+ }
+ if source
+ payload[:source_number] = source.to_s
+ payload[:source_number_type] = "ALPHANUMERIC"
+ end
+ payload
+ end
+
+ response = post("/v1/messages", body: {
+ messages: numbers,
+ }.to_json, headers: {
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ })
+
+ raise "request failed with #{response.status_code}" unless response.status_code == 202
+ nil
+ end
+end
diff --git a/drivers/message_media/sms_spec.cr b/drivers/message_media/sms_spec.cr
new file mode 100644
index 00000000000..537be2d7cea
--- /dev/null
+++ b/drivers/message_media/sms_spec.cr
@@ -0,0 +1,27 @@
+DriverSpecs.mock_driver "MessageMedia::SMS" do
+ # Send the request
+ retval = exec(:send_sms,
+ phone_numbers: "+61418419954",
+ message: "hello steve"
+ )
+
+ # sms should send a HTTP request
+ expect_http_request do |request, response|
+ headers = request.headers
+ io = request.body
+ if io
+ data = io.gets_to_end
+ request = JSON.parse(data)
+ if request["messages"][0]["content"] == "hello steve" && headers["Authorization"]? == "Basic #{Base64.strict_encode("srvc_acct:password!")}"
+ response.status_code = 202
+ else
+ response.status_code = 401
+ end
+ else
+ raise "expected request to include dialing details #{request.inspect}"
+ end
+ end
+
+ # What the sms function should return
+ retval.get.should eq(nil)
+end
diff --git a/drivers/microsoft/FindMe Service API.TXT b/drivers/microsoft/FindMe Service API.TXT
new file mode 100644
index 00000000000..d0a726c550e
--- /dev/null
+++ b/drivers/microsoft/FindMe Service API.TXT
@@ -0,0 +1,58 @@
+*List of available buildings and levels. Returns number of people "findable" on each level*
+
+GET /FindMeService/api/MeetingRooms/BuildingLevelsWithMeetingRooms
+
+[{"Building":"SYDNEY","Level":"0","Online":13},{"Building":"SYDNEY","Level":"2","Online":14},{"Building":"SYDNEY","Level":"3","Online":18}]
+
+*List of meeting rooms in a building/level*
+
+GET /FindMeService/api/MeetingRooms/Level/SYDNEY/2
+
+[{"Alias":"cf2020","Name":"Minogue","Building":"SYDNEY","Level":"2","LocationDescription":"2020","X":null,"Y":null,"Capacity":4,"Features":null,"CanBeBooked":true,"PhotoUrl":null,"HasAV":false,"HasDeskPhone":true,"HasSpeakerPhone":false,"HasWhiteboard":true}]
+
+You need to honour the CanBeBooked flag - don't try to book room that can't be booked!
+
+*Meetings for all rooms on a given level*
+
+Due to the design of our kiosk and web site we always get all meetings for all rooms.
+
+GET /FindMeService/api/MeetingRooms/Meetings/SYDNEY/2/2015-11-12T02:11:41/2015-11-15T02:11:41
+GET /FindMeService/api/MeetingRooms/Meetings/Building/Level/StartDate/EndDate
+
+[{"ConferenceRoomAlias":"cfsydinx","Start":"2015-11-11T23:30:00+00:00","End":"2015-11-12T00:00:00+00:00","Subject":"","Location":"Pty MR Syd L2 INXS (10) RT Int","BookingUserAlias":null,"StartTimeZoneName":null,"EndTimeZoneName":null},{"ConferenceRoomAlias":"cfsydinx","Start":"2015-11-12T23:00:00+00:00","End":"2015-11-13T00:00:00+00:00","Subject":"","Location":"Pty MR Syd L2 INXS (10) RT Int","BookingUserAlias":null,"StartTimeZoneName":null,"EndTimeZoneName":null},{"ConferenceRoomAlias":"cfsydsky","Start":"2015-11-13T01:00:00+00:00","End":"2015-11-13T03:00:00+00:00","Subject":"","Location":"Sydney team: Pty MR Syd L2 Skyhooks (10) RT","BookingUserAlias":null,"StartTimeZoneName":null,"EndTimeZoneName":null}]
+
+*Schedule a Meeting*
+
+POST /FindMeService/api/MeetingRooms/ScheduleMeeting
+
+{"ConferenceRoomAlias":"cf2205","Start":"2015-11-13T18:00:00","End":"2015-11-13T18:30:00","Subject":" String?,
+ headers : Hash(String, String) | HTTP::Headers = HTTP::Headers.new
+ ) : String
+ headers["Authorization"] = @auth_token unless @auth_token.empty?
+ response = http(method, path, body, params, headers)
+
+ if response.status_code == 401 && response.headers["WWW-Authenticate"]?
+ supported = response.headers.get("WWW-Authenticate")
+ raise "doesn't support NTLM auth: #{supported}" unless supported.includes?("NTLM")
+
+ # Negotiate NTLM
+ headers["Authorization"] = NTLM.negotiate_http(@domain)
+ response = http(method, path, body, params, headers)
+
+ # Extract the challenge
+ raise "unexpected response #{response.status_code}" unless response.status_code == 401 && response.headers["WWW-Authenticate"]?
+ challenge = response.headers["WWW-Authenticate"]
+
+ # Authenticate the client
+ @auth_token = NTLM.authenticate_http(challenge, @username, @password)
+ headers["Authorization"] = @auth_token
+ response = http(method, path, body, params, headers)
+ end
+
+ raise "request #{path} failed with status: #{response.status_code}" unless response.success?
+
+ response.body
+ end
+
+ def levels
+ data = make_request("GET", "/FindMeService/api/MeetingRooms/BuildingLevelsWithMeetingRooms")
+ logger.debug { "levels request returned: #{data}" }
+
+ levels = Array(Microsoft::Level).from_json(data)
+ buildings = Hash(String, Array(String)).new { |hash, key| hash[key] = [] of String }
+ levels.each { |level| buildings[level.building] << level.name }
+
+ buildings
+ end
+
+ def user_details(usernames : String | Array(String))
+ users = usernames.is_a?(String) ? [usernames] : usernames
+ data = make_request("GET", "/FindMeService/api/ObjectLocation/Users/#{users.join(",")}?getExtendedData=true")
+
+ logger.debug { "user details request returned #{data}" }
+
+ Array(Microsoft::Location).from_json(data).reject { |loc| {"NoRecentData", "NoData"}.includes?(loc.status) }
+ end
+
+ def users_on(building : String, level : String)
+ # Same response as above with or without ExtendedUserData
+ uri = "/FindMeService/api/ObjectLocation/Level/#{building}/#{level}"
+ # uri += "?getExtendedData=true" if extended_data
+
+ data = make_request("GET", uri)
+
+ begin
+ Array(Microsoft::Location).from_json(data).reject { |loc| {"NoRecentData", "NoData"}.includes?(loc.status) }
+ rescue error
+ logger.debug { "failed to parse location data\n#{data}" }
+ raise error
+ end
+ end
+end
diff --git a/drivers/microsoft/find_me_location_service.cr b/drivers/microsoft/find_me_location_service.cr
new file mode 100644
index 00000000000..e800f579529
--- /dev/null
+++ b/drivers/microsoft/find_me_location_service.cr
@@ -0,0 +1,198 @@
+module Microsoft; end
+
+require "json"
+require "oauth2"
+require "s2_cells"
+require "placeos-driver/interface/locatable"
+require "./find_me_models"
+
+class Microsoft::FindMeLocationService < PlaceOS::Driver
+ include Interface::Locatable
+
+ descriptive_name "FindMe Location Service"
+ generic_name :FindMeLocationService
+ description %(collects desk usage and wireless locations for visualising on a map)
+
+ accessor findme : FindMe_1
+
+ default_settings({
+ map_id_prefix: "table-",
+
+ floor_mappings: {
+ "zone-id": {
+ building: "SYDNEY",
+ level: "L14",
+ },
+ },
+
+ building_zone: "zone-building",
+ s2_level: 21,
+ })
+
+ @building_zone : String = ""
+ @floor_mappings : Hash(String, NamedTuple(building: String, level: String)) = {} of String => NamedTuple(building: String, level: String)
+ @zone_filter : Array(String) = [] of String
+ @map_id_prefix : String = "table-"
+ @s2_level : Int32 = 21
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @map_id_prefix = setting?(String, :map_id_prefix).presence || "table-"
+
+ @building_zone = setting(String, :building_zone)
+ @floor_mappings = setting(Hash(String, NamedTuple(building: String, level: String)), :floor_mappings)
+ @zone_filter = @floor_mappings.keys
+ @s2_level = setting?(Int32, :s2_level) || 21
+ end
+
+ # ===================================
+ # Locatable Interface functions
+ # ===================================
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "searching for #{email}, #{username}" }
+
+ locations_raw = findme.user_details(username).get.to_json
+ locations = Array(Microsoft::Location).from_json locations_raw
+
+ locations = locations.compact_map do |location|
+ coords = location.coordinates
+ next unless coords
+
+ level = findme_building = findme_level = ""
+ @floor_mappings.each do |zone, details|
+ findme_building = details[:building]
+ findme_level = details[:level]
+
+ if findme_building == coords.building && findme_level == coords.level
+ level = zone
+ break
+ end
+ end
+
+ next if level.empty?
+
+ build_location_response(location, level, findme_building, findme_level)
+ end
+
+ locations
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ logger.debug { "listing MAC addresses assigned to #{email}, #{username}" }
+
+ active_users_raw = findme.user_details(username || email).get.to_json
+ active_users = Array(Microsoft::Location).from_json active_users_raw
+
+ found = [] of String
+ if user_details = active_users[0]?
+ found << user_details.username
+ end
+ found
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ logger.debug { "searching for owner of #{mac_address}" }
+
+ active_users_raw = findme.user_details(mac_address).get.to_json
+ active_users = Array(Microsoft::Location).from_json active_users_raw
+
+ if user_details = active_users[0]?
+ {
+ location: user_details.located_using == "FixedLocation" ? "desk" : "wireless",
+ assigned_to: user_details.user_data.not_nil!.email_address || "",
+ mac_address: mac_address,
+ }
+ end
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching devices in zone #{zone_id}" }
+ return [] of Nil unless @zone_filter.includes?(zone_id)
+
+ findme_details = @floor_mappings[zone_id]?
+ return [] of Nil unless findme_details
+
+ findme_building = findme_details[:building]
+ findme_level = findme_details[:level]
+ active_users_raw = findme.users_on(findme_building, findme_level).get.to_json
+ active_users = Array(Microsoft::Location).from_json active_users_raw
+
+ locations = active_users.compact_map do |loc|
+ build_location_response(loc, zone_id, findme_building, findme_level, location)
+ end
+
+ locations
+ end
+
+ protected def build_location_response(location, zone_id, findme_building, findme_level, loc_type = nil)
+ case location.located_using
+ when "FixedLocation"
+ return if loc_type.presence && loc_type != "desk"
+
+ location_id = "#{@map_id_prefix}#{location.location_id}"
+
+ loc = {
+ location: :desk,
+ at_location: 1,
+ map_id: location_id,
+ level: zone_id,
+ building: @building_zone,
+ mac: location.username,
+ last_seen: location.last_update.to_unix,
+ capacity: 1,
+
+ findme_building: findme_building,
+ findme_level: findme_level,
+ findme_status: location.status,
+ findme_type: location.type,
+ }
+
+ loc
+ when "WiFi"
+ return if loc_type.presence && loc_type != "wireless"
+
+ coordinates = location.coordinates
+ return unless coordinates
+
+ if gps = location.gps
+ lat = gps.latitude
+ lon = gps.longitude
+ end
+
+ # Based on the confidence % and a max variance of 20m
+ variance = 20 - (20 * (location.confidence / 100))
+
+ loc = {
+ location: :wireless,
+ coordinates_from: "top-left",
+ x: coordinates.x,
+ y: coordinates.y,
+ # x,y coordinates are % based so map width and height are out of 100
+ map_width: 100,
+ # by not returning map height, it indicates that a relative height should be calculated
+ # map_height: 100,
+ lon: lon,
+ lat: lat,
+ s2_cell_id: lat ? S2Cells::LatLon.new(lat.not_nil!, lon.not_nil!).to_token(@s2_level) : nil,
+
+ mac: location.username,
+ variance: variance,
+
+ last_seen: location.last_update.to_unix,
+ level: zone_id,
+ building: @building_zone,
+
+ findme_building: findme_building,
+ findme_level: findme_level,
+ findme_status: location.status,
+ findme_type: location.type,
+ }
+ else
+ logger.info { "unexpected location type #{location.located_using}" }
+ nil
+ end
+ end
+end
diff --git a/drivers/microsoft/find_me_location_service_spec.cr b/drivers/microsoft/find_me_location_service_spec.cr
new file mode 100644
index 00000000000..a5e94949941
--- /dev/null
+++ b/drivers/microsoft/find_me_location_service_spec.cr
@@ -0,0 +1,83 @@
+DriverSpecs.mock_driver "Microsoft::FindMeLocationService" do
+ system({
+ FindMe: {FindMe},
+ })
+
+ now = Time.local
+ start = now.at_beginning_of_day.to_unix
+ ending = now.at_end_of_day.to_unix
+
+ resp = exec(:device_locations, "zone-id").get
+ puts resp
+ resp.should eq([
+ {
+ "location" => "wireless",
+ "coordinates_from" => "top-left",
+ "x" => 76.0,
+ "y" => 29.0,
+ "map_width" => 100,
+ "lon" => 151.1382508278,
+ "lat" => -33.796597429,
+ "s2_cell_id" => "6b12a5f8f0c4",
+ "mac" => "dwatson",
+ "variance" => 0.0,
+ "last_seen" => 1447295150,
+ "level" => "zone-id",
+ "building" => "zone-building",
+ "findme_building" => "SYDNEY",
+ "findme_level" => "L14",
+ "findme_status" => "Located",
+ "findme_type" => "Person",
+ }, {
+ "location" => "desk",
+ "at_location" => 1,
+ "map_id" => "table-11.097",
+ "level" => "zone-id",
+ "building" => "zone-building",
+ "mac" => "acorder003",
+ "last_seen" => 1608185586,
+ "capacity" => 1,
+ "findme_building" => "SYDNEY",
+ "findme_level" => "L14",
+ "findme_status" => "NoRecentData",
+ "findme_type" => "Person",
+ },
+ ])
+end
+
+class FindMe < DriverSpecs::MockDriver
+ def user_details(usernames : String | Array(String))
+ JSON.parse %([{"Alias":"dwatson","LastUpdate":"2015-11-12T02:25:50.017Z","Confidence":100,
+ "Coordinates":{"Building":"SYDNEY","Level":"L14","X":76,"Y":29,"LocationDescription":"2140","MapByLocationId":true},
+ "GPS":{"Latitude":-33.796597429,"Longitude":151.1382508278,"Accuracy":0.0,"LocationDescription":null},
+ "LocationIdentifier":null,"Status":"Located","LocatedUsing":"FixedLocation","Type":"Person","Comments":null,
+ "ExtendedUserData":{"Alias":"dwatson","DisplayName":"David Watson","EmailAddress":"David.Watson@microsoft.com","LyncSipAddress":"dwatson@microsoft.com"}}])
+ end
+
+ def users_on(building : String, level : String)
+ # Wireless and a desk
+ JSON.parse %([{"Alias":"dwatson","LastUpdate":"2015-11-12T02:25:50.017Z","Confidence":100,
+ "Coordinates":{"Building":"SYDNEY","Level":"L14","X":76,"Y":29,"LocationDescription":"2140","MapByLocationId":true},
+ "GPS":{"Latitude":-33.796597429,"Longitude":151.1382508278,"Accuracy":0.0,"LocationDescription":null},
+ "LocationIdentifier":null,"Status":"Located","LocatedUsing":"WiFi","Type":"Person","Comments":null,
+ "ExtendedUserData":{"Alias":"dwatson","DisplayName":"David Watson","EmailAddress":"David.Watson@microsoft.com","LyncSipAddress":"dwatson@microsoft.com"}},
+
+ {
+ "Alias": "acorder003",
+ "LastUpdate": "2020-12-17T06:13:06.797Z",
+ "CurrentUntil": "2020-12-17T06:16:06.797Z",
+ "Confidence": 100,
+ "Coordinates": null,
+ "GPS": null,
+ "LocationIdentifier": "11.097",
+ "Status": "NoRecentData",
+ "LocatedUsing": "FixedLocation",
+ "Type": "Person",
+ "Comments": null,
+ "ExtendedUserData": null,
+ "WiFiScale": 1.00,
+ "userTypes": []
+ }
+ ])
+ end
+end
diff --git a/drivers/microsoft/find_me_models.cr b/drivers/microsoft/find_me_models.cr
new file mode 100644
index 00000000000..a1a4c84999f
--- /dev/null
+++ b/drivers/microsoft/find_me_models.cr
@@ -0,0 +1,108 @@
+require "json"
+
+module Microsoft
+ class Level
+ include JSON::Serializable
+
+ @[JSON::Field(key: "Building")]
+ getter building : String
+
+ @[JSON::Field(key: "Level")]
+ getter name : String
+
+ @[JSON::Field(key: "Online")]
+ getter online : Int32
+ end
+
+ class Coordinates
+ include JSON::Serializable
+
+ @[JSON::Field(key: "Building")]
+ getter building : String
+
+ @[JSON::Field(key: "Level")]
+ getter level : String
+
+ @[JSON::Field(key: "X")]
+ getter x : Float64
+
+ @[JSON::Field(key: "Y")]
+ getter y : Float64
+ end
+
+ class GPS
+ include JSON::Serializable
+
+ @[JSON::Field(key: "Latitude")]
+ getter latitude : Float64
+
+ @[JSON::Field(key: "Longitude")]
+ getter longitude : Float64
+ end
+
+ class UserData
+ include JSON::Serializable
+
+ @[JSON::Field(key: "Alias")]
+ getter username : String?
+
+ @[JSON::Field(key: "DisplayName")]
+ getter display_name : String?
+
+ @[JSON::Field(key: "EmailAddress")]
+ getter email_address : String?
+ end
+
+ # Example Response:
+ # [{"Alias":"dwatson","LastUpdate":"2015-11-12T02:25:50.017Z","Confidence":100,
+ # "Coordinates":{"Building":"SYDNEY","Level":"2","X":76,"Y":29,"LocationDescription":"2140","MapByLocationId":true},
+ # "GPS":{"Latitude":-33.796597429,"Longitude":151.1382508278,"Accuracy":0.0,"LocationDescription":null},
+ # "LocationIdentifier":null,"Status":"Located","LocatedUsing":"FixedLocation","Type":"Person","Comments":null,
+ # "ExtendedUserData":{"Alias":"dwatson","DisplayName":"David Watson","EmailAddress":"David.Watson@microsoft.com","LyncSipAddress":"dwatson@microsoft.com"}}]
+ class Location
+ include JSON::Serializable
+
+ module RFC3339Converter
+ def self.from_json(value : JSON::PullParser) : Time
+ Time::Format::RFC_3339.parse(value.read_string)
+ end
+
+ def self.to_json(value : Time, json : JSON::Builder)
+ json.string(Time::Format::RFC_3339.format(value, 1))
+ end
+ end
+
+ @[JSON::Field(key: "Alias")]
+ getter username : String
+
+ @[JSON::Field(
+ key: "LastUpdate",
+ converter: Microsoft::Location::RFC3339Converter
+ )]
+ getter last_update : Time
+
+ @[JSON::Field(key: "Confidence")]
+ getter confidence : Float64
+
+ @[JSON::Field(key: "Coordinates")]
+ getter coordinates : Coordinates?
+
+ @[JSON::Field(key: "GPS")]
+ getter gps : GPS?
+
+ @[JSON::Field(key: "LocationIdentifier")]
+ getter location_id : String?
+
+ @[JSON::Field(key: "Status")]
+ getter status : String
+
+ @[JSON::Field(key: "LocatedUsing")]
+ getter located_using : String?
+
+ @[JSON::Field(key: "Type")]
+ getter type : String?
+
+ @[JSON::Field(key: "ExtendedUserData")]
+ getter user_data : UserData?
+ end
+end
diff --git a/drivers/microsoft/find_me_spec.cr b/drivers/microsoft/find_me_spec.cr
new file mode 100644
index 00000000000..79ae028abd0
--- /dev/null
+++ b/drivers/microsoft/find_me_spec.cr
@@ -0,0 +1,33 @@
+DriverSpecs.mock_driver "Microsoft::FindMe" do
+ # Send the request
+ retval = exec(:levels)
+
+ # sms should send a HTTP request
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response << %([{"Building":"SYDNEY","Level":"0","Online":13},{"Building":"SYDNEY","Level":"2","Online":14}])
+ end
+
+ # What the sms function should return
+ retval.get.should eq({
+ "SYDNEY" => ["0", "2"],
+ })
+
+ # Send the request
+ retval = exec(:user_details, "mbenz")
+ details_response = %([{"Alias":"mbenz","LastUpdate":"2020-12-15T13:22:00.8675244Z","CurrentUntil":"0001-01-01T00:00:00","Confidence":0,"Coordinates":null,"GPS":null,"LocationIdentifier":null,"Status":"NoData","LocatedUsing":null,"Type":null,"Comments":null,"ExtendedUserData":null,"WiFiScale":0.0,"userTypes":null}])
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response << details_response
+ end
+ retval.get.should eq([] of JSON::Any)
+
+ # Check the time format works
+ retval = exec(:user_details, "mbenz")
+ details_response = %([{"Alias":"mbenz","LastUpdate":"2020-12-15T13:22:00Z","CurrentUntil":"0001-01-01T00:00:00","Confidence":0,"Coordinates":null,"GPS":null,"LocationIdentifier":null,"Status":"NoData","LocatedUsing":null,"Type":null,"Comments":null,"ExtendedUserData":null,"WiFiScale":0.0,"userTypes":null}])
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response << details_response
+ end
+ retval.get.should eq([] of JSON::Any)
+end
diff --git a/drivers/mulesoft/booking_api.cr b/drivers/mulesoft/booking_api.cr
new file mode 100644
index 00000000000..bcfac783992
--- /dev/null
+++ b/drivers/mulesoft/booking_api.cr
@@ -0,0 +1,180 @@
+module MuleSoft; end
+
+require "./models"
+
+class MuleSoft::BookingsAPI < PlaceOS::Driver
+ descriptive_name "MuleSoft Bookings API"
+ generic_name :Bookings
+ description %(Retrieves and creates bookings using the MuleSoft API)
+ uri_base "https://api.sydney.edu.au"
+
+ default_settings({
+ venue_code: "venue code",
+ base_path: "/usyd-edu-timetable-exp-api-v1/v1/",
+ polling_cron: "*/30 7-20 * * *",
+ time_zone: "Australia/Sydney",
+ ssl_key: "private key",
+ ssl_cert: "certificate",
+ ssl_auth_enabled: true,
+ username: "basic auth username",
+ password: "basic auth password",
+ basic_auth_enabled: true,
+ running_a_spec: false,
+ })
+
+ @username : String = ""
+ @password : String = ""
+ @base_path : String = ""
+ @context : OpenSSL::SSL::Context::Client = OpenSSL::SSL::Context::Client.new
+ @host : String = ""
+ @venue_code : String = ""
+ @bookings : Array(Booking) = [] of Booking
+ @time_zone : Time::Location = Time::Location.load("Australia/Sydney")
+ @ssl_auth_enabled : Bool = false
+ @basic_auth_enabled : Bool = false
+ @runing_a_spec : Bool = false
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ schedule.clear
+ @running_a_spec = !!setting(Bool, :running_a_spec)
+
+ @username = setting(String, :username)
+ @password = setting(String, :password)
+ @basic_auth_enabled = !!setting?(Bool, :basic_auth_enabled)
+ logger.debug { "basic_auth_enabled is #{@basic_auth_enabled}" }
+
+ @base_path = setting(String, :base_path)
+ @venue_code = setting(String, :venue_code)
+
+ @host = URI.parse(config.uri.not_nil!).host.not_nil!
+
+ time_zone = setting?(String, :time_zone).presence
+ @time_zone = Time::Location.load(time_zone) if time_zone
+
+ @ssl_auth_enabled = !!setting?(Bool, :ssl_auth_enabled)
+ save_ssl_credentials if @ssl_auth_enabled
+ logger.debug { "ssl_auth_enabled is #{@ssl_auth_enabled}" }
+
+ schedule.in(Random.rand(60).seconds + Random.rand(1000).milliseconds) { poll_bookings }
+
+ cron_string = setting?(String, :polling_cron).presence || "*/30 7-20 * * *"
+ schedule.cron(cron_string, @time_zone) { poll_bookings(random_delay: true) }
+ end
+
+ def poll_bookings(random_delay : Bool = false)
+ now = Time.local @time_zone
+ from = now - 1.week
+ to = now + 1.week
+
+ logger.debug { "polling bookings #{@venue_code}, from #{from}, to #{to}, in #{@time_zone.name}" }
+ if random_delay
+ logger.debug { "random delay of <30seconds to reduce instantaneous Mulesoft API load" }
+ sleep Random.rand(30.0)
+ end
+ query_bookings(@venue_code, from, to)
+
+ check_current_booking
+ end
+
+ def check_current_booking
+ now = Time.utc.to_unix
+ previous_booking = nil
+ current_booking = nil
+ next_booking = Int32::MAX
+
+ @bookings.each_with_index do |event, index|
+ starting = event.event_start
+
+ # All meetings are in the future
+ if starting > now
+ next_booking = index
+ previous_booking = index - 1 if index > 0
+ break
+ end
+
+ # Calculate event end time
+ ending_unix = event.event_end
+
+ # Event ended in the past
+ next if ending_unix < now
+
+ # We've found the current event
+ if starting <= now && ending_unix > now
+ current_booking = index
+ previous_booking = index - 1 if index > 0
+ next_booking = index + 1
+ break
+ end
+ end
+
+ if next_booking >= (@bookings.size - 1)
+ next_booking = nil
+ end
+
+ self[:previous_booking] = previous_booking ? @bookings[previous_booking].to_placeos : nil
+ self[:current_booking] = current_booking ? @bookings[current_booking].to_placeos : nil
+ self[:next_booking] = next_booking ? @bookings[next_booking].to_placeos : nil
+ end
+
+ def query_bookings(venue_code : String, starts_at : Time = Time.local.at_beginning_of_day, ends_at : Time = Time.local.at_end_of_day)
+ client = HTTP::Client.new(host: @host, tls: (@ssl_auth_enabled ? @context : nil))
+
+ params = {
+ "startDateTime" => starts_at.to_s("%FT%T"),
+ "endDateTime" => ends_at.to_s("%FT%T"),
+ }.join('&') { |k, v| "#{k}=#{v}" }
+
+ headers = HTTP::Headers{
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ }
+
+ if @basic_auth_enabled
+ headers.add("Authorization", "Basic #{Base64.strict_encode("#{@username}:#{@password}")}")
+ end
+
+ if @running_a_spec
+ response = get("#{@base_path}/venues/#{venue_code}/bookings?#{params}", headers: headers)
+ else
+ response = client.get("#{@base_path}/venues/#{venue_code}/bookings?#{params}", headers: headers)
+ end
+
+ raise "request failed with #{response.status_code}: #{response.body}" unless (200...300).includes?(response.status_code)
+
+ # when there's no results, it seems to return just an empty response rather than an empty array?
+ if response.body.presence != nil
+ results = BookingResults.from_json(response.body)
+
+ self[:venue_code] = results.venue_code
+ self[:venue_name] = results.venue_name
+
+ @bookings = results.bookings.sort { |a, b| a.event_start <=> b.event_start }
+ self[:bookings] = @bookings.map(&.to_placeos)
+ else
+ self[:venue_code] = nil
+ self[:venue_name] = nil
+ self[:bookings] = nil
+ end
+ end
+
+ def query_bookings_epoch(venue_code : String, starts_at : Int32, ends_at : Int32)
+ query_bookings(venue_code, Time.unix(starts_at), Time.unix(ends_at))
+ end
+
+ protected def save_ssl_credentials
+ [:ssl_key, :ssl_cert].each do |key|
+ raise "Required setting #{key} left blank" unless setting(String, key).presence
+
+ File.open("./pkey-#{module_id}.#{key}", "w") do |cert|
+ cert.puts setting(String, key)
+ end
+ end
+
+ @context.private_key = "./pkey-#{module_id}.ssl_key"
+ @context.certificate_chain = "./pkey-#{module_id}.ssl_cert"
+ end
+end
diff --git a/drivers/mulesoft/booking_api_spec.cr b/drivers/mulesoft/booking_api_spec.cr
new file mode 100644
index 00000000000..d5a80797570
--- /dev/null
+++ b/drivers/mulesoft/booking_api_spec.cr
@@ -0,0 +1,50 @@
+DriverSpecs.mock_driver "MuleSoft::API" do
+ settings({
+ venue_code: "venue code",
+ base_path: "/usyd-edu-timetable-exp-api-v1/v1/",
+ polling_period: 5,
+ time_zone: "Australia/Sydney",
+ ssl_key: "private key",
+ ssl_cert: "certificate",
+ ssl_auth_enabled: false,
+ username: "basic auth username",
+ password: "basic auth password",
+ basic_auth_enabled: false,
+ running_a_spec: true,
+ })
+
+ resp = exec(:query_bookings, "A14.02.K2.05")
+
+ expect_http_request do |_request, response|
+ starts_at = Time.local - 30.minutes
+ ends_at = starts_at + 1.hour
+
+ response.status_code = 200
+ response << <<-RESPONSE
+ {
+ "count": 1,
+ "timeTableBookingsCount": 1,
+ "casualBookingsCount": 0,
+ "venueCode": "A14.02.K2.05",
+ "venueName": "A14.02.K2.05.The Quadrangle.The Quad General Lecture Theatre K2.05",
+ "bookings": [
+ {
+ "unitCode": "HSTY2630",
+ "unitName": "Panics and Pandemics",
+ "activityName": "HSTY2630-S1C-ND-CC/TUT/01",
+ "activityType": "Tutorial",
+ "activityDescription": "Tutorial",
+ "startDateTime": "#{starts_at.to_s("%FT%T")}",
+ "endDateTime": "#{ends_at.to_s("%FT%T")}",
+ "location": "Social Sciences Building - SSB Seminar Room 210",
+ "bookingType": "timeTable"
+ }
+ ]
+ }
+ RESPONSE
+ end
+
+ resp.get
+
+ exec(:check_current_booking).get
+end
diff --git a/drivers/mulesoft/calendar_exporter.cr b/drivers/mulesoft/calendar_exporter.cr
new file mode 100644
index 00000000000..d61b6c30916
--- /dev/null
+++ b/drivers/mulesoft/calendar_exporter.cr
@@ -0,0 +1,115 @@
+module MuleSoft; end
+
+require "./models"
+require "place_calendar"
+
+class MuleSoft::CalendarExporter < PlaceOS::Driver
+ descriptive_name "MuleSoft Bookings to Calendar Events Exporter"
+ generic_name :MulesoftExport
+ description %(Listens for new MuleSoft bookings and creates matching Events in a Calendar)
+
+ default_settings({
+ calendar_time_zone: "Australia/Sydney",
+ })
+
+ accessor calendar : Calendar_1
+
+ @time_zone_string : String | Nil = "Australia/Sydney"
+ @time_zone : Time::Location = Time::Location.load("Australia/Sydney")
+ @bookings : Array(Hash(String, Int64 | String | Nil)) = [] of Hash(String, Int64 | String | Nil)
+ @existing_events : Array(JSON::Any) = [] of JSON::Any
+ # An array of Attendee that has only the system (room) email address. Generally static
+ @just_this_system : NamedTuple(email: String, name: String) = {email: "", name: ""}
+
+ def on_load
+ @just_this_system = {
+ "email": system.email.not_nil!,
+ "name": system.name,
+ }
+ on_update
+ end
+
+ def on_update
+ subscriptions.clear
+
+ @time_zone_string = setting?(String, :calendar_time_zone).presence
+ @time_zone = Time::Location.load(@time_zone_string.not_nil!) if @time_zone_string
+ self[:timezone] = @time_zone.to_s
+
+ subscription = system.subscribe(:Bookings_1, :bookings) do |_subscription, mulesoft_bookings|
+ logger.debug { "DETECTED change in Mulesoft Bookings..." }
+ @bookings = Array(Hash(String, Int64 | String | Nil)).from_json(mulesoft_bookings)
+ logger.debug { "#{@bookings.size} bookings in total" }
+ self[:total_bookings] = @bookings.size
+
+ update_events
+ @bookings.each { |b| export_booking(b) }
+ end
+ end
+
+ def status
+ {
+ "bookings": @bookings,
+ "events": @existing_events,
+ }
+ end
+
+ def update_events
+ logger.debug { "FETCHING existing Calendar events..." }
+ @existing_events = fetch_events()
+ logger.debug { "#{@existing_events.size} events in total" }
+ self[:total_events] = @existing_events.size
+ end
+
+ protected def fetch_events(past_span : Time::Span = 14.days, future_span : Time::Span = 14.days)
+ now = Time.local @time_zone
+ from = now - past_span
+ til = now + future_span
+
+ calendar.list_events(
+ calendar_id: system.email.not_nil!,
+ period_start: from.to_unix,
+ period_end: til.to_unix
+ ).get.as_a
+ end
+
+ protected def export_booking(mulesoft_booking : Hash(String, Int64 | String | Nil))
+ # The resulting Calendar Event is to be slightly different to the Mulesoft Booking, so clone here and transform
+ booking = mulesoft_booking.clone
+ # Add the course code to the front of the booking title/body
+ booking["title"] = "#{booking["recurring_master_id"]} #{booking["title"] || booking["body"]}"
+ logger.debug { "Checking for existing events that match: #{booking}" }
+
+ unless event_already_exists?(booking, @existing_events)
+ new_event = {
+ title: booking["title"],
+ event_start: booking["event_start"],
+ event_end: booking["event_end"],
+ timezone: @time_zone_string,
+ description: booking["body"],
+ user_id: system.email.not_nil!,
+ attendees: [@just_this_system],
+ location: system.name.not_nil!,
+ }
+ logger.debug { ">>> EXPORTING booking as: #{new_event}" }
+ calendar.create_event(**new_event)
+ end
+ end
+
+ protected def event_already_exists?(booking : Hash(String, Int64 | String | Nil), existing_events : Array(JSON::Any))
+ existing_events.any? { |existing_event| booking_matches_event?(booking, existing_event.as_h) }
+ end
+
+ protected def booking_matches_event?(booking : Hash(String, Int64 | String | Nil), event : Hash(String, JSON::Any))
+ booking.select("event_start", "event_end", "title") == event.select("event_start", "event_end", "title")
+ end
+
+ def delete_all_events(past_days : Int32 = 14, future_days : Int32 = 14)
+ events = fetch_events(past_span: past_days.days, future_span: future_days.days)
+ events.each do |event|
+ calendar.delete_event(calendar_id: system.email.not_nil!, event_id: event["id"])
+ end
+ logger.debug { "DELETED #{events.size} events" }
+ events.size
+ end
+end
diff --git a/drivers/mulesoft/models.cr b/drivers/mulesoft/models.cr
new file mode 100644
index 00000000000..b25c66c4027
--- /dev/null
+++ b/drivers/mulesoft/models.cr
@@ -0,0 +1,61 @@
+module MuleSoft
+ class Booking
+ include JSON::Serializable
+
+ @[JSON::Field(key: "unitName")]
+ property title : String?
+
+ @[JSON::Field(key: "activityType")]
+ property body : String
+
+ @[JSON::Field(key: "unitCode")]
+ property recurring_master_id : String?
+
+ @[JSON::Field(key: "startDateTime", converter: MuleSoft::DateTimeConvertor)]
+ property event_start : Int64
+
+ @[JSON::Field(key: "endDateTime", converter: MuleSoft::DateTimeConvertor)]
+ property event_end : Int64
+
+ property location : String
+
+ # we need this method to create an intermediary hash
+ # otherwise when to_json is called all the field names revert to the MuleSoft ones
+ def to_placeos
+ value = {
+ "title" => @title,
+ "body" => @body,
+ "recurring_master_id" => @recurring_master_id,
+ "event_start" => @event_start,
+ "event_end" => @event_end,
+ "location" => @location,
+ }
+ end
+ end
+
+ class BookingResults
+ include JSON::Serializable
+
+ property count : Int64
+
+ @[JSON::Field(key: "venueCode")]
+ property venue_code : String
+
+ @[JSON::Field(key: "venueName")]
+ property venue_name : String
+
+ property bookings : Array(Booking)
+ end
+
+ module DateTimeConvertor
+ extend self
+
+ def to_json(value, json : JSON::Builder)
+ json.string(Time.unix(value).to_local.to_s("%FT%T"))
+ end
+
+ def from_json(pull : JSON::PullParser)
+ Time.parse(pull.read_string, "%FT%T", Time::Location.local).to_unix
+ end
+ end
+end
diff --git a/drivers/nec/display.cr b/drivers/nec/display.cr
new file mode 100644
index 00000000000..5a4e68691ed
--- /dev/null
+++ b/drivers/nec/display.cr
@@ -0,0 +1,288 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+class Nec::Display < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::AudioMuteable
+
+ enum Input
+ Vga = 1
+ Rgbhv = 2
+ Dvi = 3
+ HdmiSet = 4
+ Video1 = 5
+ Video2 = 6
+ Svideo = 7
+ Tuner = 9
+ Tv = 10
+ Dvd1 = 12
+ Option = 13
+ Dvd2 = 14
+ DisplayPort = 15
+ Hdmi = 17
+ Hdmi2 = 18
+ Hdmi3 = 130
+ Usb = 135
+ end
+ include PlaceOS::Driver::Interface::InputSelection(Input)
+
+ # Discovery Information
+ tcp_port 7142
+ descriptive_name "NEC Display"
+ generic_name :Display
+
+ DELIMITER = 0x0D_u8
+
+ def on_load
+ # Communication settings
+ queue.delay = 120.milliseconds
+ queue.timeout = 5.seconds
+ transport.tokenizer = Tokenizer.new(Bytes[DELIMITER])
+ end
+
+ def connected
+ schedule.clear
+ schedule.every(50.seconds, true) do
+ do_poll
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool)
+ # Do nothing if already in desired state
+ return if self[:power]? == state
+
+ if state
+ logger.debug { "requested to power on" }
+ # 1 = Power On
+ data = MsgType::Command.build(Command::SetPower, 1)
+ send(data, name: "power", delay: 5.seconds)
+ else
+ logger.debug { "requested to power off" }
+ # 4 = Power Off
+ data = MsgType::Command.build(Command::SetPower, 4)
+ send(data, name: "power", delay: 10.seconds, timeout: 10.seconds)
+ end
+ end
+
+ def power?(**options) : Bool
+ data = MsgType::Command.build(Command::PowerQuery)
+ send(data, **options, name: "power?").get
+ self[:power].as_bool
+ end
+
+ def switch_to(input : Input)
+ logger.debug { "requested to switch to: #{input}" }
+ data = MsgType::SetParameter.build(Command::VideoInput, input.value)
+ send(data, name: "input", delay: 6.seconds)
+ end
+
+ enum Audio
+ Audio1 = 1
+ Audio2 = 2
+ Audio3 = 3
+ Hdmi = 4
+ Tv = 6
+ DisplayPort1 = 7
+ DisplayPort2 = 8
+ Hdmi2 = 10
+ Hdmi3 = 11
+ MultiPicture = 13
+ ComputeModule = 14
+ end
+
+ def switch_audio(input : Audio)
+ logger.debug { "requested to switch audio to: #{input}" }
+ data = MsgType::SetParameter.build(Command::AudioInput, input.value)
+ send(data, name: "audio")
+ end
+
+ def auto_adjust
+ data = MsgType::SetParameter.build(Command::AutoSetup, 1)
+ send(data, name: "auto_adjust")
+ end
+
+ def brightness(val : Int32)
+ data = MsgType::SetParameter.build(Command::BrightnessStatus, val.clamp(0, 100))
+ send(data, name: "brightness")
+ send(MsgType::Command.build(Command::Save), name: "save", priority: 0)
+ end
+
+ def contrast(val : Int32)
+ data = MsgType::SetParameter.build(Command::ContrastStatus, val.clamp(0, 100))
+ send(data, name: "contrast")
+ send(MsgType::Command.build(Command::Save), name: "save", priority: 0)
+ end
+
+ def volume(val : Int32)
+ data = MsgType::SetParameter.build(Command::VolumeStatus, val.clamp(0, 100))
+ send(data, name: "volume")
+ send(MsgType::Command.build(Command::Save), name: "save", priority: 0)
+ end
+
+ def mute_audio(state : Bool = true, index : Int32 | String = 0)
+ logger.debug { "requested to update mute to #{state}" }
+ data = MsgType::SetParameter.build(Command::MuteStatus, state ? 1 : 0)
+ send(data, name: "mute_audio")
+ end
+
+ def do_poll
+ current_power = power?(priority: 0)
+ logger.debug { "Polling, power = #{current_power}" }
+
+ if current_power
+ mute_status
+ volume_status
+ video_input
+ audio_input
+ end
+ end
+
+ def received(data, task)
+ header = data[0..6]
+ message = data[7..-3]
+ checksum = data[-2]
+
+ unless checksum == data[1..-3].reduce { |a, b| a ^ b }
+ return task.try &.retry("invalid checksum in device response")
+ end
+
+ begin
+ case MsgType.from_value header[4]
+ when .command_reply?
+ parse_command_reply message
+ when .get_parameter_reply?, .set_parameter_reply?
+ parse_response message
+ else
+ raise "unknown message type"
+ end
+ rescue e
+ task.try &.abort e.message
+ else
+ task.try &.success
+ end
+ end
+
+ # Command replies each use a different packet structure
+ private def parse_command_reply(message : Bytes)
+ # Don't do any processing if this is the response for the save command
+ return if (string = String.new(message[1..-2])) == "00C"
+ response = string.hexbytes
+
+ if response[1..3] == Bytes[0xC2, 0x03, 0xD6] # Set power
+ result_code = response[0]
+ raise "unsupported operation" unless result_code == 0
+ self[:power] = response[5] == 1
+ elsif response[2..3] == Bytes[0xD6, 0x00] # Power query
+ result_code = response[1]
+ raise "unsupported operation" unless result_code == 0
+ self[:power] = response[7] == 1
+ else
+ logger.warn { "unhandled command reply: #{message}" }
+ end
+ end
+
+ # Get and set parameter replies share common structure
+ private def parse_response(message : Bytes)
+ response = String.new(message[1..-2]).hexbytes
+
+ result_code = response[0]
+ raise "unsupported operation" unless result_code == 0
+
+ op_code = response[1].to_u16 << 8 | response[2]
+ value = response[6].to_u16 << 8 | response[7]
+
+ case Command.from_value op_code
+ when .video_input?
+ self[:input] = Input.from_value(value)
+ when .audio_input?
+ self[:audio] = Audio.from_value(value)
+ when .volume_status?
+ self[:volume] = value
+ self[:audio_mute] = value == 0
+ when .brightness_status?
+ self[:brightness] = value
+ when .contrast_status?
+ self[:contrast] = value
+ when .mute_status?
+ self[:audio_mute] = value == 1
+ self[:volume] = 0 if value == 1
+ when .auto_setup?
+ # auto_setup
+ # nothing needed to do here (we are delaying the next command by 4 seconds)
+ else
+ logger.warn { "unhandled device response: #{message}" }
+ end
+ end
+
+ enum Command
+ VideoInput = 0x0060
+ AudioInput = 0x022E
+ VolumeStatus = 0x0062
+ MuteStatus = 0x008D
+ PowerOnDelay = 0x02D8
+ ContrastStatus = 0x0012
+ BrightnessStatus = 0x0010
+ AutoSetup = 0x001E
+ PowerQuery = 0x01D6
+ Save = 0x0C
+ SetPower = 0xC203D6
+
+ def to_s : String
+ case self
+ when .save?
+ length = 2
+ when .set_power?
+ length = 6
+ else
+ length = 4
+ end
+ value.to_s(16, upcase: true).rjust(length, '0')
+ end
+ end
+
+ {% for name in Command.constants %}
+ @[Security(Level::Administrator)]
+ def {{name.id.underscore}}(priority : Int32 = 0)
+ send(MsgType::GetParameter.build(Command::{{name.id}}), priority: priority, name: {{name.id.underscore.stringify}})
+ end
+ {% end %}
+
+ # Types of messages sent to and from the LCD
+ enum MsgType : UInt8
+ Command = 0x41 # 'A'
+ CommandReply = 0x42 # 'B'
+ GetParameter = 0x43 # 'C'
+ GetParameterReply = 0x44 # 'D'
+ SetParameter = 0x45 # 'E'
+ SetParameterReply = 0x46 # 'F'
+
+ def build(command : Nec::Display::Command, data : Int? = nil)
+ command = command.to_s
+
+ message = String.build do |str|
+ str << "0*0"
+ str.write_byte self.value # Type
+
+ message_length = command.size + 2
+ message_length += 4 if data # If there is data, add 4 to the message length
+ str << message_length.to_s(16, upcase: true).rjust(2, '0') # Message length
+ str.write_byte 0x02 # Start of messsage
+ str << command # Message
+ str << data.to_s(16, upcase: true).rjust(4, '0') if data # Data if required
+ str.write_byte 0x03 # End of message
+ end
+
+ String.build do |str|
+ str.write_byte 0x01 # SOH
+ str << message # Message
+ str.write_byte message.each_byte.reduce { |a, b| a ^ b } # Checksum
+ str.write_byte DELIMITER # Delimiter
+ end
+ end
+ end
+end
diff --git a/drivers/nec/display_spec.cr b/drivers/nec/display_spec.cr
new file mode 100644
index 00000000000..39a60a8c63f
--- /dev/null
+++ b/drivers/nec/display_spec.cr
@@ -0,0 +1,70 @@
+DriverSpecs.mock_driver "Nec::Display" do
+ # do_poll
+ # power?
+ should_send("\x010*0A06\x0201D6\x03\x1F\x0D")
+ responds("\x0100*B12\x020200D60000040001\x03\x1F\x0D")
+ status[:power].should eq(true)
+ # mute_status
+ should_send("\x010*0C06\x02008D\x03\x12\x0D")
+ responds("\x0100*D12\x0200008D0000000002\x03\x12\x0D")
+ status[:audio_mute].should eq(false)
+ # volume_status
+ should_send("\x010*0C06\x020062\x03\x6A\x0D")
+ responds("\x0100*D12\x020000620000000032\x03\x69\x0D")
+ status[:volume].should eq(50)
+ # video_input
+ should_send("\x010*0C06\x020060\x03\x68\x0D")
+ responds("\x0100*D12\x020000600000000011\x03\x6A\x0D")
+ status[:input].should eq("Hdmi")
+ # audio_input
+ should_send("\x010*0C06\x02022E\x03\x1B\x0D")
+ responds("\x0100*D12\x0200022E0000000001\x03\x18\x0D")
+ status[:audio].should eq("Audio1")
+
+ exec(:mute_audio)
+ should_send("\x010*0E0A\x02008D0001\x03\x62\x0D")
+ responds("\x0100*F12\x0200008D0000000001\x03\x13\x0D")
+ status[:audio_mute].should eq(true)
+ status[:volume].should eq(0)
+
+ exec(:unmute_audio)
+ should_send("\x010*0E0A\x02008D0000\x03\x63\x0D")
+ responds("\x0100*F12\x0200008D0000000000\x03\x12\x0D")
+ status[:audio_mute].should eq(false)
+
+ exec(:volume, 25)
+ should_send("\x010*0E0A\x0200620019\x03\x13\x0D")
+ responds("\x0100*F12\x020000620000640019\x03\x60\x0D")
+ should_send("\x010*0A04\x020C\x03\x1D\x0D")
+ responds("\x0100*B06\x0200C\x03\x2C\x0D")
+ status[:audio_mute].should eq(false)
+ status[:volume].should eq(25)
+
+ exec(:brightness_status)
+ should_send("\x010*0C06\x020010\x03\x6F\x0D")
+ responds("\x0100*D12\x020000100000000000\x03\x6D\x0D")
+ status[:brightness].should eq(0)
+
+ exec(:brightness, 100)
+ should_send("\x010*0E0A\x0200100064\x03\x1C\x0D")
+ responds("\x0100*F12\x020000100000640064\x03\x6F\x0D")
+ should_send("\x010*0A04\x020C\x03\x1D\x0D")
+ responds("\x0100*B06\x0200C\x03\x2C\x0D")
+ status[:brightness].should eq(100)
+
+ exec(:switch_to, "tv")
+ should_send("\x010*0E0A\x020060000A\x03\x68\x0D")
+ responds("\x0100*F12\x02000060000000000A\x03\x19\x0D")
+ status[:input].should eq("Tv")
+
+ exec(:switch_audio, "audio_2")
+ sleep 6 # since switch_to has 6 seconds of delay
+ should_send("\x010*0E0A\x02022E0002\x03\x68\x0D")
+ responds("\x0100*F12\x0200022E0000000002\x03\x19\x0D")
+ status[:audio].should eq("Audio2")
+
+ exec(:power, false)
+ should_send("\x010*0A0C\x02C203D60004\x03\x1D\x0D")
+ responds("\x0100*B0E\x0200C203D60004\x03\x18\x0D")
+ status[:power].should eq(false)
+end
diff --git a/drivers/nec/np_series.cr b/drivers/nec/np_series.cr
new file mode 100644
index 00000000000..443ad5dbc9a
--- /dev/null
+++ b/drivers/nec/np_series.cr
@@ -0,0 +1,465 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+class Nec::Projector < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Input
+ VGA = 0x01
+ RGBHV = 0x02
+ Composite = 0x06
+ SVideo = 0x0B
+ Component = 0x10
+ Component2 = 0x11
+ HDMI = 0x1A
+ HDMI2 = 0x1B
+ DisplayPort = 0xA6
+ LAN = 0x20
+ Viewer = 0x1F
+ end
+
+ include Interface::InputSelection(Input)
+
+ # Discovery Information
+ tcp_port 7142
+ descriptive_name "NEC Projector"
+ generic_name :Display
+
+ default_settings({
+ volume_min: 0,
+ volume_max: 63,
+ })
+
+ @power_target : Bool? = nil
+ @input_target : Input? = nil
+ @volume_min : Int32 = 0
+ @volume_max : Int32 = 63
+
+ def on_load
+ # Communication settings
+ queue.delay = 100.milliseconds
+ self[:error] = [] of String
+ on_update
+ end
+
+ def on_update
+ @power_target = nil
+ @input_target = nil
+ @volume_min = setting(Int32, :volume_min)
+ @volume_max = setting(Int32, :volume_max)
+ end
+
+ def connected
+ schedule.every(50.seconds, true) { do_poll }
+ end
+
+ def disconnected
+ schedule.clear
+ # Disconnect often occurs on power off
+ # We may have not received a status response before the disconnect occurs
+ self[:power] = false
+ end
+
+ # Command Listing
+ # Second byte used to detect command type
+ COMMAND = {
+ # Mute controls
+ mute_picture: Bytes[0x02, 0x10, 0x00, 0x00, 0x00, 0x12],
+ unmute_picture: Bytes[0x02, 0x11, 0x00, 0x00, 0x00, 0x13],
+ mute_audio_cmd: Bytes[0x02, 0x12, 0x00, 0x00, 0x00, 0x14],
+ unmute_audio_cmd: Bytes[0x02, 0x13, 0x00, 0x00, 0x00, 0x15],
+ mute_onscreen: Bytes[0x02, 0x14, 0x00, 0x00, 0x00, 0x16],
+ unmute_onscreen: Bytes[0x02, 0x15, 0x00, 0x00, 0x00, 0x17],
+
+ freeze_picture: Bytes[0x01, 0x98, 0x00, 0x00, 0x01, 0x01],
+ unfreeze_picture: Bytes[0x01, 0x98, 0x00, 0x00, 0x01, 0x02],
+
+ lamp?: Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81], # Running sense (ret 81)
+ input?: Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x02], # Input status (ret 85)
+ mute?: Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x03], # MUTE STATUS REQUEST (Check 10H on byte 5)
+ error?: Bytes[0x00, 0x88, 0x00, 0x00, 0x00, 0x88], # ERROR STATUS REQUEST (ret 88)
+ model?: Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x04], # Request model name (both of these are related)
+
+ # lamp hours / remaining info
+ lamp_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D], # LAMP INFORMATION REQUEST
+ filter_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D],
+ projector_info: Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D],
+
+ # TODO: figure out where these are in the docs as they conflict with audio_switch
+ background_black: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x01], # set mute to be a black screen
+ background_blue: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x00], # set mute to be a blue screen
+ background_logo: Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x02], # set mute to be the company logo
+ }
+
+ {% for name, data in COMMAND %}
+ def {{name.id}}(**options)
+ do_send(COMMAND[{{name.id.stringify}}], **options, name: {{name.id.stringify}})
+ end
+ {% end %}
+
+ def volume(vol : Int32)
+ vol = vol.clamp(@volume_min, @volume_max)
+ # volume base command D1 D2 D3 D4 D5
+ command = Bytes[0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, vol, 0x00]
+ # D3 = 00 (absolute vol) or 01 (relative vol)
+ # D4 = value (lower bits 0 to 63)
+ # D5 = value (higher bits always 00h)
+
+ do_send(command)
+ end
+
+ # Mutes both audio/video
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ if layer.video? || layer.audio_video?
+ if state
+ mute_picture
+ mute_onscreen
+ else
+ unmute_picture
+ end
+ end
+
+ if layer.audio? || layer.audio_video?
+ state ? mute_audio_cmd : unmute_audio_cmd
+ end
+ end
+
+ def switch_to(input : Input)
+ logger.debug { "-- NEC projector, requested to switch to: #{input}" }
+ @input_target = input
+ command = Bytes[0x02, 0x03, 0x00, 0x00, 0x02, 0x01, input.value]
+ do_send(command, name: "input")
+ end
+
+ enum Audio
+ HDMI
+ VGA # Computer in docs
+ end
+
+ def switch_audio(input : Audio)
+ # C0 == HDMI Audio
+ command = Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0xC0, input.value]
+ do_send(command, name: "switch_audio")
+ end
+
+ def power(state : Bool)
+ @power_target = state
+
+ if state
+ command = Bytes[0x02, 0x00, 0x00, 0x00, 0x00]
+ do_send(command, name: "power", timeout: 15.seconds, delay: 1.second)
+ else
+ command = Bytes[0x02, 0x01, 0x00, 0x00, 0x00]
+ # Jump ahead of any other queued commands as they are no longer important
+ do_send(
+ command,
+ name: "power",
+ timeout: 60.seconds, # don't want retries occuring very fast
+ delay: 30.seconds,
+ clear_queue: true,
+ priority: 100,
+ )
+ end
+ end
+
+ def power?(**options) : Bool
+ do_send(COMMAND[:lamp?], **options, name: "power?").get
+ !!self[:power]?.try(&.as_bool)
+ end
+
+ def switch_to(input : Input)
+ @input_target = input
+ command = Bytes[0x02, 0x03, 0x00, 0x00, 0x02, 0x01, input.value]
+ do_send(command, name: "input")
+ end
+
+ def do_poll
+ if power?(priority: 0)
+ mute?(priority: 0)
+ background_black(priority: 0)
+ lamp_info(priority: 0)
+ end
+ end
+
+ private def checksum_valid?(data : Bytes)
+ checksum = data[0..-2].sum(0) & 0xFF
+ logger.debug { "Error: checksum should be 0x#{checksum.to_s(16, upcase: true)}" } unless result = checksum == data[-1]
+ result
+ end
+
+ private def do_send(command : Bytes, **options)
+ req = Bytes.new(command.size + 1)
+ req.copy_from(command)
+ req[-1] = (command.sum(0) & 0xFF).to_u8
+ logger.debug { "Nec proj sending 0x#{req.hexstring}" }
+ send(req, **options) { |data, task| process_response(data, task, req) }
+ end
+
+ # TODO: add responses for freeze commands if we need to process them
+ enum Response : UInt16
+ Power = 8321 # [0x20,0x81]
+ InputOrMuteQuery = 8325 # [0x20,0x85]
+ Error = 8328 # [0x20,0x88]
+ InputSwitch = 8707 # [0x22,0x03]
+ Lamp = 8704 # [0x22,0x00]
+ Lamp2 = 8705 # [0x22,0x01]
+ PictureMuteOn = 8720 # [0x22,0x10]
+ PictureMuteOff = 8721 # [0x22,0x11]
+ AudioMuteOn = 8722 # [0x22,0x12]
+ AudioMuteOff = 8723 # [0x22,0x13]
+ OnscreenMuteOn = 8724 # [0x22,0x14]
+ OnscreenMuteOff = 8725 # [0x22,0x15]
+ VolumeOrImageAdjust = 8976 # [0x23,0x10]
+ Info = 9098 # [0x23,0x8A]
+ AudioSwitch = 9137 # [0x23,0xB1]
+
+ def self.from_bytes?(response)
+ value = IO::Memory.new(response[0..1]).read_bytes(UInt16, IO::ByteFormat::BigEndian)
+ Response.from_value?(value)
+ end
+ end
+
+ private def process_response(data, task, req = nil)
+ logger.debug { "NEC projector sent: 0x#{data.hexstring}" }
+
+ # Command failed
+ if (data[0] & 0xA0) == 0xA0
+ # We were changing power state at time of failure we should keep trying
+ if req && (0..1).includes?(req[1])
+ # command[:delay_on_receive] = 6000
+ power?
+ return task.try(&.success)
+ end
+ return task.try(&.abort("-- NEC projector, sent fail code for command: 0x#{req.try(&.hexstring) || "unknown"}"))
+ end
+
+ # Verify checksum
+ unless checksum_valid?(data)
+ return task.try(&.abort("-- NEC projector, checksum failed for command: 0x#{req.try(&.hexstring) || "unknown"}"))
+ end
+
+ # Only process response if successful
+ # Otherwise return success to prevent retries on commands we were not expecting
+ unless resp = Response.from_bytes?(data)
+ return task.try(&.success("-- NEC projector, no status updates defined for response for command: 0x#{req.try(&.hexstring) || "unknown"}"))
+ end
+
+ case resp
+ when .power?
+ process_power_status(data)
+ when .input_or_mute_query?
+ # Return if we can't work out what was requested initially
+ return task.try(&.success) unless req && (2..3).includes?(req[-2])
+ process_input_state(data) if req[-2] == 2
+ process_mute_state(data) if req[-2] == 3
+ when .error?
+ process_error_status(data)
+ when .input_switch?
+ return process_input_switch(data, task, req)
+ when .lamp?, .lamp2?
+ process_lamp_command(data, req)
+ when .picture_mute_on?, .picture_mute_off?
+ self[:mute] = self[:picture_mute] = resp.picture_mute_on?
+ when .audio_mute_on?, .audio_mute_off?
+ self[:audio_mute] = resp.audio_mute_on?
+ when .onscreen_mute_on?, .onscreen_mute_off?
+ self[:onscreen_mute] = resp.onscreen_mute_on?
+ when .volume_or_image_adjust?
+ self[:volume] = req[-3] if req && data[-3] == 5 && data[-2] == 0
+ # We don't care about image adjust
+ when .info?
+ process_projector_info(data)
+ when .audio_switch? # TODO: also seems to the seem as setting background response
+ self[:audio_input] = Audio.from_value(data[-2]) if data[-3] == 0xC0
+ end
+
+ task.try(&.success)
+ end
+
+ def received(data, task)
+ process_response(data, task)
+ end
+
+ # Process the lamp status response
+ # Intimately entwined with the power power command
+ # (as we need to control ensure we are in the correct target state)
+ private def process_power_status(data)
+ logger.debug { "-- NEC projector sent a response to a power status command" }
+
+ self[:power] = (data[-2] & 0b10) > 0
+
+ # Projector cooling || power on off processing
+ if (data[-2] & 0b100000) > 0 || (data[-2] & 0b10000000) > 0
+ if @power_target
+ self[:cooling] = false
+ self[:warming] = true
+ logger.debug { "power warming..." }
+ else
+ self[:warming] = false
+ self[:cooling] = true
+ logger.debug { "power cooling..." }
+ end
+
+ schedule.in(3.seconds) { power? }
+ # Signal processing
+ elsif (data[-2] & 0b1000000) > 0
+ schedule.in(3.seconds) { power? }
+ else # We are in a stable state!
+ if power_target = @power_target
+ if self[:power] == power_target
+ @power_target = nil
+ else # We are in an undesirable state and will try to correct it
+ logger.debug { "NEC projector in an undesirable power state... (Correcting)" }
+ power(power_target)
+ end
+ else
+ logger.debug { "NEC projector is in a good power state..." }
+ self[:warming] = self[:cooling] = false
+ # Ensure the input is in the correct state if power/lamp is on
+ input? if self[:power].as_bool # Calls status mute
+ end
+ end
+
+ logger.debug { "Current state {power: #{self[:power]}, warming: #{self[:warming]}, cooling: #{self[:cooling]}}" }
+ end
+
+ # NEC has different values for the input status when compared to input selection
+ INPUT_MAP = {
+ 0x01 => {
+ 0x01 => Input::VGA,
+ 0x02 => Input::Composite,
+ 0x03 => Input::SVideo,
+ 0x06 => Input::HDMI,
+ 0x07 => Input::Viewer,
+ 0x21 => Input::HDMI,
+ 0x22 => Input::DisplayPort,
+ },
+ 0x02 => {
+ 0x01 => Input::RGBHV,
+ 0x04 => Input::Component2,
+ 0x06 => Input::HDMI2,
+ 0x07 => Input::LAN,
+ 0x21 => Input::HDMI2,
+ },
+ 0x03 => {
+ 0x04 => Input::Component,
+ },
+ }
+
+ private def process_input_state(data)
+ return unless self[:power]?.try(&.as_bool) && (first = INPUT_MAP[data[-15]])
+
+ logger.debug { "-- NEC projector sent a response to an input state command" }
+
+ self[:input] = current_input = first[data[-14]] || "unknown"
+ if data[-17] == 0x01
+ # TODO: figure out how to write in crystal and if needed
+ # command[:delay_on_receive] = 3000 # still processing signal
+ input?
+ else # TODO: figure out if this is needed from old ruby driver
+ # mute? # get mute status one signal has settled
+ end
+
+ logger.debug { "The input selected was: #{current_input}" }
+
+ # Notify of bad input selection for debugging
+ # We ensure at the very least power state and input are always correct
+ if (input_target = @input_target)
+ # If we have reached the input_target, clear @input_target so input can be set again
+ if current_input == input_target
+ @input_target = nil
+ else
+ logger.debug { "-- NEC input state may not be correct, desired: #{input_target} current: #{current_input}" }
+ switch_to(input_target)
+ end
+ end
+ end
+
+ private def process_mute_state(data)
+ logger.debug { "-- NEC projector responded to mute state command" }
+ self[:mute] = self[:picture_mute] = data[-17] == 0x01
+ self[:audio_mute] = data[-16] == 0x01
+ self[:onscreen_mute] = data[-15] == 0x01
+ end
+
+ private def process_input_switch(data, task, req)
+ logger.debug { "-- NEC projector responded to switch input command" }
+ if data[-2] != 0xFF
+ input? # Double check with a status update
+ return task.try(&.success)
+ end
+ task.try(&.retry("-- NEC projector failed to switch input with command: #{req.try(&.hexstring) || "unknown"}"))
+ end
+
+ private def process_lamp_command(data, req)
+ logger.debug { "-- NEC projector sent a response to a power command" }
+ # Ensure a change of power state was the last command sent
+ if req && (0..1).includes?(req[1])
+ power? # Queues the status power command
+ end
+ end
+
+ # Provide all the error info required
+ ERROR_CODES = [{
+ 0b1 => "Lamp cover error",
+ 0b10 => "Temperature error (Bimetal)",
+ # 0b100 => not used
+ 0b1000 => "Fan Error",
+ 0b10000 => "Fan Error",
+ 0b100000 => "Power Error",
+ 0b1000000 => "Lamp Error",
+ 0b10000000 => "Lamp has reached its end of life",
+ }, {
+ 0b1 => "Lamp has been used beyond its limit",
+ 0b10 => "Formatter error",
+ 0b100 => "Lamp no.2 Error",
+ }, {
+ # 0b1 => "not used"
+ 0b10 => "FPGA error",
+ 0b100 => "Temperature error (Sensor)",
+ 0b1000 => "Lamp housing error",
+ 0b10000 => "Lamp data error",
+ 0b100000 => "Mirror cover error",
+ 0b1000000 => "Lamp no.2 has reached its end of life",
+ 0b10000000 => "Lamp no.2 has been used beyond its limit",
+ }, {
+ 0b1 => "Lamp no.2 housing error",
+ 0b10 => "Lamp no.2 data error",
+ 0b100 => "High temperature due to dust pile-up",
+ 0b1000 => "A foreign object sensor error",
+ }]
+
+ private def process_error_status(data)
+ logger.debug { "-- NEC projector sent a response to an error status command" }
+ errors = [] of String
+ # Run through each byte
+ data[5..8].each_with_index do |byte, byte_no|
+ # If there is an error
+ if byte > 0
+ # Go through each individual bit
+ ERROR_CODES[byte_no].each_key do |bit_check|
+ # Add the error if the bit corresponding to it is set
+ errors.push(ERROR_CODES[byte_no][bit_check]) if (bit_check & byte) > 0
+ end
+ end
+ end
+ self[:error] = errors
+ end
+
+ private def process_projector_info(data)
+ logger.debug { "-- NEC projector sent a response to a projector info command" }
+ # Calculate lamp/filter usage in seconds
+ lamp = data[87..90].each_with_index.sum { |byte, index| byte.to_i << (index * 8) }
+ filter = data[91..94].each_with_index.sum { |byte, index| byte.to_i << (index * 8) }
+ # Convert seconds to hours
+ self[:lamp_usage] = lamp / 3600
+ self[:filter_usage] = filter / 3600
+ logger.debug { "lamp usage is #{self[:lamp_usage]} hours, filter usage is #{self[:filter_usage]} hours" }
+ end
+end
diff --git a/drivers/nec/np_series_spec.cr b/drivers/nec/np_series_spec.cr
new file mode 100644
index 00000000000..b06ff72d165
--- /dev/null
+++ b/drivers/nec/np_series_spec.cr
@@ -0,0 +1,83 @@
+# NOTES
+# (*1) Projector ID
+# (*2) Model code: "xxH" inscription
+# (*3) Checksum: "CKS" inscription
+# (*4) Response error number
+# (*5) Term “RGB” and “COMPUTER”
+# (*6) Term “DVI” and “COMPUTER”
+
+DriverSpecs.mock_driver "Nec::Projector" do
+ p_id = 0x00_u8 # Projector ID
+ mdlc = 0x10_u8 # Model code
+
+ # do_poll
+ # power?
+ should_send(Bytes[0x00, 0x81, 0x00, 0x00, 0x00, 0x81, 0x02])
+ responds(Bytes[0x20, 0x81, p_id, mdlc, 0x10, 0b_0000_0010, 0xC3])
+ status[:power].should eq(true)
+ # input?
+ should_send(Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x02, 0x88])
+ responds(Bytes[0x20, 0x85, p_id, mdlc, 0x10,
+ # Data, simplified for sanity
+ # We only care about the ones with 0x
+ # -17 -15 -14
+ 0x00, 2, 0x01, 0x06, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 0x4C]) # Checksum
+ status[:input].should eq("HDMI")
+ # mute?
+ should_send(Bytes[0x00, 0x85, 0x00, 0x00, 0x01, 0x03, 0x89])
+ responds(Bytes[0x20, 0x85, p_id, mdlc, 0x10,
+ # -17 -16 -15
+ 0x00, 0x00, 0x00, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 0x47]) # Checksum
+ status[:mute].should eq(false)
+ status[:picture_mute].should eq(false)
+ status[:audio_mute].should eq(false)
+ status[:onscreen_mute].should eq(false)
+ # background_black
+ should_send(Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0x0B, 0x01, 0xC2])
+ responds(Bytes[0x23, 0xB1, p_id, mdlc, 0x02, 0x0B, 0xF1])
+ # lamp_info
+ should_send(Bytes[0x03, 0x8A, 0x00, 0x00, 0x00, 0x8D, 0x1A])
+ # 5 for header, 1 for checksum and 98 for data
+ response = Bytes.new(104)
+ response.copy_from(Bytes[0x23, 0x8A, p_id, mdlc, 0x62, 0x0B]) # header
+ # data
+ # lamp usage
+ response[87] = 0xC0
+ response[88] = 0x65
+ response[89] = 0x52
+ # filter usage
+ response[92] = 0xE4
+ response[93] = 0x57
+ # checksum
+ response[-1] = 0xDC
+ responds(response)
+ status[:lamp_usage].should eq(1500)
+ status[:filter_usage].should eq(1600)
+
+ exec(:volume, 100)
+ should_send(Bytes[0x03, 0x10, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x3F, 0x00, 0x5C])
+ responds(Bytes[0x23, 0x10, p_id, mdlc, 0x05, 0x00, 0x48])
+ status[:volume].should eq(63)
+
+ exec(:mute)
+ # mute_picture
+ should_send(Bytes[0x02, 0x10, 0x00, 0x00, 0x00, 0x12, 0x24])
+ responds(Bytes[0x22, 0x10, p_id, mdlc, 0x32, 0x00, 0x74])
+ status[:mute] = true
+ status[:picture_mute] = true
+ # mute_onscreen
+ should_send(Bytes[0x02, 0x14, 0x00, 0x00, 0x00, 0x16, 0x2C])
+ responds(Bytes[0x22, 0x14, p_id, mdlc, 0x00, 0x46])
+ status[:onscreen_mute] = true
+ # mute_audio
+ should_send(Bytes[0x02, 0x12, 0x00, 0x00, 0x00, 0x14, 0x28])
+ responds(Bytes[0x22, 0x12, p_id, mdlc, 0x00, 0x44])
+ status[:audio_mute] = true
+
+ exec(:switch_audio, "VGA")
+ should_send(Bytes[0x03, 0xB1, 0x00, 0x00, 0x02, 0xC0, 0x01, 0x77])
+ responds(Bytes[0x23, 0xB1, p_id, mdlc, 0xC0, 0x01, 0xA5])
+ status[:audio_input].should eq("VGA")
+end
diff --git a/drivers/office_rnd/models.cr b/drivers/office_rnd/models.cr
new file mode 100644
index 00000000000..f6943dffc4d
--- /dev/null
+++ b/drivers/office_rnd/models.cr
@@ -0,0 +1,202 @@
+require "json"
+
+# OfficeRnD Data Models
+module OfficeRnd
+ abstract struct Data
+ include JSON::Serializable
+ end
+
+ struct TokenResponse < Data
+ include JSON::Serializable
+ property access_token : String
+ property token_type : String
+ property expires_in : Int32
+ property scope : String
+ end
+
+ struct Office < Data
+ @[JSON::Field(key: "_id")]
+ getter id : String
+ getter name : String
+ getter country : String?
+ getter state : String?
+ getter city : String?
+ getter address : String?
+ getter timezone : String?
+ getter image : String?
+ @[JSON::Field(key: "isOpen")]
+ getter is_open : Bool?
+ end
+
+ struct BookingTime < Data
+ @[JSON::Field(key: "dateTime")]
+ getter time : Time
+
+ def initialize(@time : Time); end
+ end
+
+ struct Fee < Data
+ getter name : String
+ getter price : Int32
+ getter quantity : Int32 = 1
+ getter date : Time
+ @[JSON::Field(key: "team")]
+ getter team_id : String?
+ @[JSON::Field(key: "office")]
+ getter office_id : String
+ @[JSON::Field(key: "member")]
+ getter member_id : String?
+ @[JSON::Field(key: "plan")]
+ getter plan_id : String?
+ getter refundable : Bool?
+ @[JSON::Field(key: "billInAdvance")]
+ getter bill_in_advance : Bool?
+ @[JSON::Field(key: "isPersonal")]
+ getter is_personal : Bool?
+ end
+
+ struct BookingFee < Data
+ getter date : Time
+ getter fee : Fee?
+ @[JSON::Field(key: "extraFees")]
+ getter extra_fees : Array(JSON::Any?)
+ getter credits : Array(Credit)
+ end
+
+ struct Booking < Data
+ @[JSON::Field(key: "start")]
+ getter booking_start : BookingTime
+ @[JSON::Field(key: "end")]
+ getter booking_end : BookingTime
+ getter timezone : String = "Australia/Sydney"
+ getter source : String?
+ getter summary : String?
+ @[JSON::Field(key: "resourceId")]
+ getter resource_id : String
+ @[JSON::Field(key: "plan")]
+ getter plan_id : String = ""
+ @[JSON::Field(key: "team")]
+ getter team_id : String?
+ @[JSON::Field(key: "member")]
+ getter member_id : String?
+ getter description : String?
+ getter tentative : Bool?
+ getter free : Bool?
+ getter fees : Array(::OfficeRnd::BookingFee) = [] of ::OfficeRnd::BookingFee
+ getter extras : JSON::Any = JSON::Any.new("")
+
+ def initialize(
+ @resource_id : String,
+ booking_start : Time,
+ booking_end : Time,
+ @summary : String? = nil,
+ @team_id : String? = nil,
+ @member_id : String? = nil,
+ @description : String? = nil,
+ @tentative : Bool? = nil,
+ @free : Bool? = nil
+ )
+ unless @member_id || @team_id
+ raise "Booking requires at least one of team_id or member_id"
+ end
+ @booking_start = BookingTime.new(booking_start)
+ @booking_end = BookingTime.new(booking_end)
+ end
+
+ def overlaps?(time_span : Range(Time, Time))
+ starting, ending = booking_start.time, booking_end.time
+ within = time_span.includes?(starting) || time_span.includes?(ending)
+ covers = starting < time_span.begin && ending > time_span.end
+
+ within || covers
+ end
+ end
+
+ struct Credit < Data
+ getter count : Int32
+ getter credit : String
+ end
+
+ struct Rate < Data
+ @[JSON::Field(key: "_id")]
+ getter id : String
+ getter name : String
+ getter price : Int32
+ @[JSON::Field(key: "cancellationPolicy")]
+ getter cancellation_policy : CancellationPolicy
+ getter extras : Array(Extra)
+ @[JSON::Field(key: "maxDuration")]
+ getter max_duration : Int32
+
+ struct CancellationPolicy < Data
+ @[JSON::Field(key: "minimumPeriod")]
+ property minimum_period : Int32
+ end
+
+ struct Extra < Data
+ @[JSON::Field(key: "_id")]
+ getter id : String
+ getter name : String
+ getter price : Int32
+ end
+ end
+
+ struct Resource < Data
+ getter name : String
+ @[JSON::Field(key: "rate")]
+ getter rate_id : String?
+ @[JSON::Field(key: "office")]
+ getter office_id : String
+ @[JSON::Field(key: "room")]
+ getter floor_id : String
+ getter type : Type
+
+ MAPPING = {
+ Type::MeetingRoom => "meeting_room",
+ Type::PrivateOffices => "team_room",
+ Type::PrivateOfficeDesk => "desk_tr",
+ Type::DedicatedDesks => "desk",
+ Type::HotDesks => "hotdesk",
+ }
+
+ enum Type
+ MeetingRoom
+ PrivateOffices
+ PrivateOfficeDesk
+ DedicatedDesks
+ HotDesks
+
+ def to_s
+ Resource::MAPPING[self]
+ end
+
+ def to_json(json : JSON::Builder)
+ json.string(self.to_s)
+ end
+
+ def self.parse(type : String)
+ parsed = Resource::MAPPING.key_for?(type)
+ raise ArgumentError.new("Unrecognised Resource::Type '#{type}'") unless parsed
+ parsed
+ end
+
+ def self.valid?(type : String)
+ !!(Resource::MAPPING.key_for?(type))
+ end
+ end
+ end
+
+ struct Floor < Data
+ @[JSON::Field(key: "_id")]
+ getter id : String
+ getter floor : String?
+ getter name : String
+ @[JSON::Field(key: "office")]
+ getter office_id : String
+ getter area : Int32?
+ @[JSON::Field(key: "isOpen")]
+ getter is_open : Bool?
+ @[JSON::Field(key: "targetRevenue")]
+ getter target_revenue : Int32?
+ end
+end
diff --git a/drivers/office_rnd/office_rnd_api.cr b/drivers/office_rnd/office_rnd_api.cr
new file mode 100644
index 00000000000..e5ab9c2d5e1
--- /dev/null
+++ b/drivers/office_rnd/office_rnd_api.cr
@@ -0,0 +1,268 @@
+require "uri"
+require "uuid"
+
+require "./models"
+
+module OfficeRnd
+ class OfficeRndAPI < PlaceOS::Driver
+ # Discovery Information
+ generic_name :OfficeRnd
+ descriptive_name "OfficeRnD REST API"
+
+ default_settings({
+ client_id: "10000000",
+ client_secret: "c5a6adc6-UUID-46e8-b72d-91395bce9565",
+ scopes: ["officernd.api.read", "officernd.api.write"],
+ test_auth: true,
+ })
+
+ @client_id : String = ""
+ @client_secret : String = ""
+ @scopes : Array(String) = [] of String
+
+ @test_auth : Bool = false
+ @auth_token : String = ""
+ @auth_expiry : Time = 1.minute.ago
+
+ def on_load
+ on_update
+ @test_auth = setting(Bool, :test_auth)
+ end
+
+ def on_update
+ @client_id = setting(String, :client_id)
+ @client_secret = setting(String, :client_secret)
+ @scopes = setting(Array(String), :scopes)
+ end
+
+ def expire_token!
+ @auth_expiry = 1.minute.ago
+ end
+
+ def token_expired?
+ @auth_expiry < Time.utc
+ end
+
+ def get_token
+ return @auth_token unless token_expired?
+ auth_route = @test_auth ? "http://localhost:17839/oauth/token" : "https://identity.officernd.com/oauth/token"
+ params = HTTP::Params.encode({
+ "client_id" => @client_id,
+ "client_secret" => @client_secret,
+ "grant_type" => "client_credentials",
+ "scope" => @scopes.join(' '),
+ })
+ headers = HTTP::Headers{
+ "Content-Type" => "application/x-www-form-urlencoded",
+ "Accept" => "application/json",
+ }
+ response = HTTP::Client.post(
+ url: auth_route,
+ headers: headers,
+ body: params,
+ )
+ body = response.body
+ logger.debug { "received login response: #{body}" }
+
+ if response.success?
+ resp = TokenResponse.from_json(body)
+ @auth_expiry = Time.utc + (resp.expires_in - 5).seconds
+ @auth_token = "Bearer #{resp.access_token}"
+ else
+ logger.error { "authentication failed with HTTP #{response.status_code}" }
+ raise "failed to obtain access token"
+ end
+ end
+
+ # Floor
+ ###########################################################################
+
+ # Get a floor
+ #
+ def floor(floor_id : String)
+ get_request("/floors/#{floor_id}", Floor)
+ end
+
+ # Get floors
+ #
+ def floors(office_id : String?, name : String?)
+ params = HTTP::Params.new
+ params["office"] = office_id if office_id
+ params["name"] = name if name
+ query_string = params.to_s
+ path = query_string.empty? ? "/floors" : "/floors?#{query_string}"
+ get_request(path, Array(Floor))
+ end
+
+ # Booking
+ ###########################################################################
+
+ # Get bookings for a resource for a given time span
+ #
+ def resource_bookings(
+ resource_id : String,
+ range_start : Time = Time.utc - 5.minutes,
+ range_end : Time = Time.utc + 24.hours,
+ office_id : String? = nil,
+ member_id : String? = nil,
+ team_id : String? = nil
+ ) : Array(Booking)
+ time_span = (range_start..range_end)
+ bookings(
+ office_id: office_id,
+ member_id: member_id,
+ team_id: team_id,
+ ).select! do |booking|
+ booking.resource_id == resource_id && booking.overlaps?(time_span)
+ end
+ end
+
+ # Get a booking
+ #
+ def booking(booking_id : String)
+ get_request("/bookings/#{booking_id}", Booking)
+ end
+
+ # Get bookings
+ #
+ def bookings(
+ office_id : String? = nil,
+ member_id : String? = nil,
+ team_id : String? = nil
+ )
+ params = HTTP::Params.new
+ params["office"] = office_id if office_id
+ params["member"] = member_id if member_id
+ params["team"] = team_id if team_id
+ query_string = params.to_s
+ path = query_string.empty? ? "/bookings" : "/bookings?#{query_string}"
+ get_request(path, Array(Booking))
+ end
+
+ # Delete a booking
+ #
+ def delete_booking(booking_id : String)
+ !!(delete_request("/bookings/#{booking_id}"))
+ end
+
+ # Make a booking
+ #
+ def create_bookings(bookings : Array(Booking))
+ response = post("/bookings", body: bookings.to_json, headers: {
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ "Authorization" => get_token,
+ })
+ unless response.success?
+ expire_token! if response.status_code == 401
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ # Create a booking
+ #
+ def create_booking(
+ resource_id : String,
+ booking_start : Time,
+ booking_end : Time,
+ summary : String? = nil,
+ team_id : String? = nil,
+ member_id : String? = nil,
+ description : String? = nil,
+ tentative : Bool? = nil,
+ free : Bool? = nil
+ )
+ create_bookings [Booking.new(
+ resource_id: resource_id,
+ booking_start: booking_start,
+ booking_end: booking_end,
+ summary: summary,
+ team_id: team_id,
+ member_id: member_id,
+ description: description,
+ tentative: tentative,
+ free: free,
+ )]
+ end
+
+ alias BookingArgument = NamedTuple(
+ resource_id: String,
+ booking_start: Time,
+ booking_end: Time,
+ summary: String?,
+ team_id: String?,
+ member_id: String?,
+ description: String?,
+ tentative: Bool?,
+ free: Bool?,
+ )
+
+ def create_bookings(bookings : Array(BookingArgument))
+ create_bookings(bookings.map { |booking| Booking.new(**booking) })
+ end
+
+ # Office
+ ###########################################################################
+
+ # List offices
+ #
+ def offices
+ path = "/offices"
+ get_request(path, Array(Office))
+ end
+
+ # Retrieve office
+ #
+ def office(name : String)
+ path = "/offices/#{name}"
+ get_request(path, Office)
+ end
+
+ # Resource
+ ###########################################################################
+
+ # Get available rooms (resources) by
+ # - type
+ # - date range (available_from, available_to)
+ # - office (office_id)
+ # - resource name (name)
+ def resources(
+ type : (Resource::Type | String)? = nil,
+ name : String? = nil,
+ office_id : String? = nil,
+ available_from : Time? = nil,
+ available_to : Time? = nil
+ )
+ type = Resource::Type.parse(type) if type.is_a?(String)
+ params = HTTP::Params.new
+ params["type"] = type.to_s if type
+ params["name"] = name if name
+ params["office"] = office_id if office_id
+ params["availableFrom"] = available_from.to_s if available_from
+ params["availableTo"] = available_to.to_s if available_to
+ query_string = params.to_s
+ path = query_string.empty? ? "/resources" : "/resources?#{query_string}"
+ get_request(path, Array(Resource))
+ end
+
+ # Internal Helpers
+ #############################################################################
+
+ private macro get_request(path, result_type)
+ begin
+ %token = get_token
+ %response = get({{path}}, headers: {
+ "Accept" => "application/json",
+ "Authorization" => %token
+ })
+
+ if %response.success?
+ {{result_type}}.from_json(%response.body)
+ else
+ expire_token! if %response.status_code == 401
+ raise "unexpected response #{%response.status_code}\n#{%response.body}"
+ end
+ end
+ end
+ end
+end
diff --git a/drivers/office_rnd/office_rnd_api_spec.cr b/drivers/office_rnd/office_rnd_api_spec.cr
new file mode 100644
index 00000000000..7041b457d02
--- /dev/null
+++ b/drivers/office_rnd/office_rnd_api_spec.cr
@@ -0,0 +1,39 @@
+DriverSpecs.mock_driver "OfficeRnd::OfficeRndApi" do
+ # Send the request
+ retval = exec(:get_token)
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJSRUFEIiwiV1JJVEUiXSwiZXhwIjoxNTc0MjMzNjEyLCJhdXRob3JpdGllcyI6WyJST0xFX1RSVVNURURfQ0xJRU5UIl0sImp0aSI6IjM1ZjkxYjlkLTVmZmMtNDJkYy05YWZkLTJiZTE0YjI1MmE1NCIsImNsaWVudF9pZCI6IjEwMDAwMjEzIn0.Wzrsaey5z3ShAFYKOaWmgfoRZNsk-PclSK9IRtYf4b8"
+
+ expect_http_request do |request, response|
+ case request.path
+ when "/oauth/token"
+ data = request.body
+ .try(&.gets_to_end)
+ .try(&->HTTP::Params.parse(String))
+ # The request is param encoded
+ if data && data["grant_type"] == "client_credentials" && data["client_secret"] == "c5a6adc6-UUID-46e8-b72d-91395bce9565"
+ response.status_code = 200
+ response.output.puts %({
+ "access_token": "#{token}",
+ "token_type": "Bearer",
+ "expires_in": 3599,
+ "scope": "officernd.api.read officernd.api.write"
+ })
+ else
+ response.status_code = 401
+ response.output.puts ""
+ end
+ when .starts_with?("/bookings")
+ case request.method
+ when "DELETE"
+ # TODO: Delete booking mock response
+ when "GET"
+ # TODO: Get bookings mock response
+ when "POST"
+ # TODO: Create bookings mock response
+ end
+ end
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq("Bearer #{token}")
+end
diff --git a/drivers/panasonic/display/protocol2.cr b/drivers/panasonic/display/protocol2.cr
new file mode 100644
index 00000000000..099254f288f
--- /dev/null
+++ b/drivers/panasonic/display/protocol2.cr
@@ -0,0 +1,269 @@
+require "digest/md5"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/switchable"
+
+# Based off the very similar https://github.com/PlaceOS/drivers/blob/master/drivers/panasonic/display/nt_control.cr
+
+# How the display expects you interact with it:
+# ===============================================
+# 1. New connection required for each command sent (hence makebreak!)
+# 2. On connect, the display sends you a string of characters to use as a password salt
+# 3. Encode your message using the salt and send it to the display
+# 4. Display responds with a value
+# 5. You have to disconnect explicitly, display won't close the connection
+
+class Panasonic::Display::Protocol2 < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Inputs
+ HDMI
+ HDMI2
+ VGA
+ DVI
+ end
+
+ include Interface::InputSelection(Inputs)
+
+ # Discovery Information
+ tcp_port 1024
+ descriptive_name "Panasonic Display Protocol 2"
+ generic_name :Display
+
+ default_settings({
+ username: "admin1",
+ password: "panasonic",
+ })
+
+ makebreak!
+
+ def on_load
+ # Communication settings
+ transport.tokenizer = Tokenizer.new("\r")
+
+ schedule.every(60.seconds) { do_poll }
+
+ on_update
+ end
+
+ def disconnected
+ @channel.close unless @channel.closed?
+ end
+
+ @username : String = "admin1"
+ @password : String = "panasonic"
+
+ # used to coordinate the display password hash
+ @channel : Channel(String) = Channel(String).new
+ @power_target : Bool? = nil
+
+ def on_update
+ @username = setting?(String, :username) || "dispadmin"
+ @password = setting?(String, :password) || "@Panasonic"
+ end
+
+ COMMANDS = {
+ power_on: "PON",
+ power_off: "POF",
+ power_query: "QPW",
+ input: "IMS",
+ volume: "AVL",
+ volume_query: "QAV",
+ audio_mute: "AMT",
+ }
+ RESPONSES = COMMANDS.to_h.invert
+
+ def power(state : Bool)
+ @power_target = state
+
+ if state
+ logger.debug { "requested to power on" }
+ do_send(:power_on, retries: 10, name: :power, delay: 8.seconds)
+ else
+ logger.debug { "requested to power off" }
+ do_send(:power_off, retries: 10, name: :power, delay: 8.seconds)
+ end
+ power?
+ end
+
+ def power?(**options) : Bool
+ do_send(:power_query, **options).get
+ !!self[:power]?.try(&.as_bool)
+ end
+
+ INPUTS = {
+ Inputs::HDMI => "HM1",
+ Inputs::HDMI2 => "HM2",
+ Inputs::VGA => "PC1",
+ Inputs::DVI => "DVI",
+ }
+ INPUT_LOOKUP = INPUTS.invert
+
+ def switch_to(input : Inputs)
+ logger.debug { "requested to switch to: #{input}" }
+ do_send(:input, INPUTS[input], delay: 2.seconds)
+ self[:input] = input # for a responsive UI
+ end
+
+ # There is no input query command
+ def input?
+ self[:input]?
+ end
+
+ # There is no video mute command so this only mutes audio
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ if layer == MuteLayer::Video
+ logger.warn { "requested to mute video which is unsupported" }
+ else
+ logger.debug { "requested audio mute state: #{state}" }
+ do_send(:audio_mute, state ? 1 : 0)
+ end
+ end
+
+ def mute? : Bool
+ do_send(:audio_mute).get
+ !!self[:audio_mute]?.try(&.as_bool)
+ end
+
+ def volume(val : Int32)
+ # Unable to query current volume
+ do_send(:volume, val.to_s.rjust(3, '0')).get
+ self[:volume] = val
+ end
+
+ def volume? : Int32?
+ do_send(:volume_query).get
+ self[:volume]?.try(&.as_i)
+ end
+
+ def do_poll
+ if power?(priority: 0)
+ mute?
+ volume?
+ end
+ end
+
+ ERRORS = {
+ "ERR1" => "1: Undefined control command",
+ "ERR2" => "2: Out of parameter range",
+ "ERR3" => "3: Busy state or no-acceptable period",
+ "ERR4" => "4: Timeout or no-acceptable period",
+ "ERR5" => "5: Wrong data length",
+ "ERRA" => "A: Password mismatch",
+ "ER401" => "401: Command cannot be executed",
+ "ER402" => "402: Invalid parameter is sent",
+ }
+
+ def received(data, task)
+ data = String.new(data).strip
+ logger.debug { "Panasonic display sent: #{data} for #{task.try(&.name) || "unknown"}" }
+
+ # This is sent by the display on initial connection
+ # the channel is used to send the hash salt to the task sending a command
+ if data.starts_with?("NTCONTROL")
+ # check for protected mode
+ if @channel && !@channel.closed?
+ # 1 == protected mode
+ @channel.send(data[10] == '1' ? data[12..-1] : "")
+ else
+ transport.disconnect
+ end
+ return
+ end
+
+ # we no longer need the connection to be open , the display expects
+ # us to close it and a new connection is required per-command
+ transport.disconnect
+
+ # remove the leading 00
+ data = data[2..-1]
+
+ # Check for error response
+ if data[0] == 'E'
+ self[:last_error] = error_msg = ERRORS[data]
+
+ if {"ERR3", "ERR4"}.includes?(data)
+ logger.info { "display busy: #{error_msg} (#{data})" }
+ task.try(&.retry)
+ else
+ logger.error { "display error: #{error_msg} (#{data})" }
+ task.try(&.abort(error_msg))
+ end
+ return
+ end
+
+ # We can't interpret this message without a task reference
+ # This also makes sure it is no longer nil
+ return unless task
+
+ # Process the response
+ resp = data.split(':')
+ cmd = RESPONSES[resp[0]]?
+ val = resp[1]?
+
+ case cmd
+ when :power_on, :power_off, :power_query
+ self[:power] = cmd == :power_on if cmd == :power_on || cmd == :power_off
+ self[:power] = val.not_nil!.to_i == 1 if cmd == :power_query
+
+ # Ensure selected power state is achieved
+ if power_target = @power_target
+ if self[:power] == power_target
+ @power_target = nil
+ else
+ power(power_target)
+ end
+ end
+ when :input
+ self[:input] = INPUT_LOOKUP[val]
+ when :volume, :volume_query
+ self[:volume] = val.not_nil!.to_i
+ when :audio_mute
+ self[:audio_mute] = val.not_nil!.to_i == 1
+ end
+
+ task.success
+ end
+
+ protected def do_send(command, param = nil, **options)
+ # prepare the command
+ cmd = if param.nil?
+ "00#{COMMANDS[command]}\r"
+ else
+ "00#{COMMANDS[command]}:#{param}\r"
+ end
+
+ logger.debug { "queuing #{command}: #{cmd}" }
+
+ # queue the request
+ queue(**({
+ name: command,
+ }.merge(options))) do
+ # prepare channel and connect to the display (which will then send the random key)
+ @channel = Channel(String).new
+ transport.connect
+ # wait for the random key to arrive
+ random_key = @channel.receive
+ # build the password hash
+ password_hash = if random_key.empty?
+ # An empty key indicates unauthenticated mode
+ ""
+ else
+ Digest::MD5.hexdigest("#{@username}:#{@password}:#{random_key}")
+ end
+
+ message = "#{password_hash}#{cmd}"
+ logger.debug { "Sending: #{message}" }
+
+ # send the request
+ # NOTE:: the built in `send` function has implicit queuing, but we are
+ # in a task callback here so should be calling transport send directly
+ transport.send(message)
+ end
+ end
+end
diff --git a/drivers/panasonic/display/protocol2_spec.cr b/drivers/panasonic/display/protocol2_spec.cr
new file mode 100644
index 00000000000..4ee496bcc42
--- /dev/null
+++ b/drivers/panasonic/display/protocol2_spec.cr
@@ -0,0 +1,73 @@
+DriverSpecs.mock_driver "Panasonic::Display::Protocol2" do
+ password = "d4a58eaea919558fb54a33a2effa8b94"
+
+ # Execute a command (triggers the connection)
+ exec(:power?)
+ # Once connected the projector will send a password salt
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ # Check the request was sent with the correct password
+ should_send("#{password}00QPW\r")
+ # Respond with the status then check the state updated
+ responds("00QPW:0\r")
+ status[:power].should eq(false)
+
+ exec(:power, true)
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00PON\r")
+ responds("00PON\r")
+ sleep 8.seconds
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00QPW\r")
+ responds("00QPW:1\r")
+ status[:power].should eq(true)
+
+ exec(:switch_to, "hdmi")
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00IMS:HM1\r")
+ responds("00IMS:HM1\r")
+ status[:input].should eq("HDMI")
+
+ exec(:mute?)
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00AMT\r")
+ responds("00AMT:0\r")
+ status[:audio_mute].should eq(false)
+
+ exec(:mute)
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00AMT:1\r")
+ responds("00AMT:1\r")
+ status[:audio_mute].should eq(true)
+
+ exec(:volume?)
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00QAV\r")
+ responds("00QAV:100\r")
+ status[:volume].should eq(100)
+
+ exec(:volume, 20)
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00AVL:020\r")
+ responds("00AVL:020\r")
+ status[:volume].should eq(20)
+
+ exec(:power, false)
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00POF\r")
+ responds("00POF\r")
+ sleep 8.seconds
+ expect_reconnect
+ responds("NTCONTROL 1 09b075be\r")
+ should_send("#{password}00QPW\r")
+ responds("00QPW:0\r")
+ status[:power].should eq(false)
+end
diff --git a/drivers/panasonic/projector/nt_control.cr b/drivers/panasonic/projector/nt_control.cr
new file mode 100644
index 00000000000..cd0fdd707e1
--- /dev/null
+++ b/drivers/panasonic/projector/nt_control.cr
@@ -0,0 +1,283 @@
+require "digest/md5"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/switchable"
+
+module Panasonic; end
+
+module Panasonic::Projector; end
+
+# Documentation: https://aca.im/driver_docs/Panasonic/panasonic_pt-vw535n_manual.pdf
+# also https://aca.im/driver_docs/Panasonic/pt-ez580_en.pdf
+
+# How the projector expects you interact with it:
+# ===============================================
+# 1. New connection required for each command sent (hence makebreak!)
+# 2. On connect, the projector sends you a string of characters to use as a password salt
+# 3. Encode your message using the salt and send it to the projector
+# 4. Projector responds with a value
+# 5. You have to disconnect explicitly, projector won't close the connection
+
+class Panasonic::Projector::NTControl < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Inputs
+ HDMI
+ HDMI2
+ VGA
+ VGA2
+ Miracast
+ DVI
+ DisplayPort
+ HDBaseT
+ Composite
+ end
+
+ include PlaceOS::Driver::Interface::InputSelection(Inputs)
+
+ # Discovery Information
+ tcp_port 1024
+ descriptive_name "Panasonic Projector"
+ generic_name :Display
+ default_settings({username: "admin1", password: "panasonic"})
+ makebreak!
+
+ def on_load
+ # Communication settings
+ transport.tokenizer = Tokenizer.new("\r")
+
+ schedule.every(40.seconds) do
+ power?(priority: 0)
+ lamp_hours?(priority: 0)
+ end
+
+ on_update
+ end
+
+ def disconnected
+ @channel.close unless @channel.closed?
+ end
+
+ @username : String = "admin1"
+ @password : String = "panasonic"
+
+ # used to coordinate the projector password hash
+ @channel : Channel(String) = Channel(String).new
+ @stable_power : Bool = true
+
+ def on_update
+ @username = setting?(String, :username) || "admin1"
+ @password = setting?(String, :password) || "panasonic"
+ end
+
+ COMMANDS = {
+ power_on: "PON",
+ power_off: "POF",
+ power_query: "QPW",
+ freeze: "OFZ",
+ input: "IIS",
+ mute: "OSH",
+ lamp: "Q$S",
+ lamp_hours: "Q$L",
+ }
+ RESPONSES = COMMANDS.to_h.invert
+
+ def power(state : Bool)
+ self[:stable_power] = @stable_power = false
+ self[:power_target] = state
+
+ if state
+ logger.debug { "requested to power on" }
+ do_send(:power_on, retries: 10, name: :power, delay: 8.seconds)
+ do_send(:lamp)
+ else
+ logger.debug { "requested to power off" }
+ do_send(:power_off, retries: 10, name: :power, delay: 8.seconds).get
+
+ # Schedule this after we have a result for the power function
+ # As the projector does not even update to cooling for awhile
+ schedule.in(10.seconds) { do_send(:lamp) }
+ end
+ end
+
+ def power?(**options)
+ do_send(:lamp, **options)
+ end
+
+ def lamp_hours?(**options)
+ do_send(:lamp_hours, 1, **options)
+ end
+
+ INPUTS = {
+ Inputs::HDMI => "HD1",
+ Inputs::HDMI2 => "HD2",
+ Inputs::VGA => "RG1",
+ Inputs::VGA2 => "RG2",
+ Inputs::Miracast => "MC1",
+ Inputs::DVI => "DVI",
+ Inputs::DisplayPort => "DP1",
+ Inputs::HDBaseT => "DL1",
+ Inputs::Composite => "VID",
+ }
+ INPUT_LOOKUP = INPUTS.invert
+
+ def switch_to(input : Inputs)
+ # Projector doesn't automatically unmute
+ unmute if self[:mute]
+
+ do_send(:input, INPUTS[input], delay: 2.seconds)
+ logger.debug { "requested to switch to: #{input}" }
+
+ self[:input] = input # for a responsive UI
+ end
+
+ # Mutes audio + video
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ logger.debug { "requested mute state: #{state}" }
+
+ # TODO:: remote seems to support audio mute and AV mute
+ # but I can't find an audio mute command
+
+ actual = state ? 1 : 0
+ do_send(:mute, actual)
+ end
+
+ ERRORS = {
+ "ERR1" => "1: Undefined control command",
+ "ERR2" => "2: Out of parameter range",
+ "ERR3" => "3: Busy state or no-acceptable period",
+ "ERR4" => "4: Timeout or no-acceptable period",
+ "ERR5" => "5: Wrong data length",
+ "ERRA" => "A: Password mismatch",
+ "ER401" => "401: Command cannot be executed",
+ "ER402" => "402: Invalid parameter is sent",
+ }
+
+ def received(data, task)
+ data = String.new(data).strip
+ logger.debug { "Panasonic sent: #{data}" }
+
+ # This is sent by the projector on initial connection
+ # the channel is used to send the hash salt to the task sending a command
+ if data.starts_with? "NTCONTROL"
+ # check for protected mode
+ if @channel && !@channel.closed?
+ # 1 == protected mode
+ @channel.send(data[10] == '1' ? data[12..-1] : "")
+ else
+ transport.disconnect
+ end
+ return
+ end
+
+ # we no longer need the connection to be open , the projector expects
+ # us to close it and a new connection is required per-command
+ transport.disconnect
+
+ # Check for error response
+ if data[0] == 'E'
+ self[:last_error] = error_msg = ERRORS[data]
+
+ if {"ERR3", "ERR4"}.includes? data
+ logger.info { "projector busy: #{error_msg} (#{data})" }
+ task.try &.retry
+ else
+ logger.error { "projector error: #{error_msg} (#{data})" }
+ task.try &.abort(error_msg)
+ end
+ return
+ end
+
+ # We can't interpret this message without a task reference
+ # This also makes sure it is no longer nil
+ return unless task
+
+ # Process the response
+ data = data[2..-1]
+ resp = data.split(':')
+ cmd = RESPONSES[resp[0]]?
+ val = resp[1]?
+
+ case cmd
+ when :power_on
+ self[:power] = true
+ when :power_off
+ self[:power] = false
+ when :power_query
+ self[:power] = val.not_nil!.to_i == 1
+ when :freeze
+ self[:frozen] = val.not_nil!.to_i == 1
+ when :input
+ self[:input] = INPUT_LOOKUP[val]
+ when :mute
+ state = self[:mute] = val.not_nil!.to_i == 1
+ self[:mute0] = state
+ self[:mute0_video] = state
+ self[:mute0_audio] = state
+ else
+ case task.name
+ when "lamp"
+ ival = resp[0].to_i
+ self[:power] = {1, 2}.includes?(ival)
+ self[:warming] = ival == 1
+ self[:cooling] = ival == 3
+
+ # check target states here
+ if !@stable_power
+ if self[:power] == self[:power_target]
+ self[:stable_power] = @stable_power = true
+ else
+ power self[:power_target].as_bool
+ end
+ end
+ when "lamp_hours"
+ # Resp looks like: "001682"
+ self[:lamp_usage] = data.to_i
+ end
+ end
+
+ task.success
+ end
+
+ protected def do_send(command, param = nil, **options)
+ # prepare the command
+ cmd = if param.nil?
+ "00#{COMMANDS[command]}\r"
+ else
+ "00#{COMMANDS[command]}:#{param}\r"
+ end
+
+ logger.debug { "queuing #{command}: #{cmd}" }
+
+ # queue the request
+ queue(**({
+ name: command,
+ }.merge(options))) do
+ # prepare channel and connect to the projector (which will then send the random key)
+ @channel = Channel(String).new
+ transport.connect
+ # wait for the random key to arrive
+ random_key = @channel.receive
+ # build the password hash
+ password_hash = if random_key.empty?
+ # An empty key indicates unauthenticated mode
+ ""
+ else
+ Digest::MD5.hexdigest("#{@username}:#{@password}:#{random_key}")
+ end
+
+ message = "#{password_hash}#{cmd}"
+ logger.debug { "Sending: #{message}" }
+
+ # send the request
+ # NOTE:: the built in `send` function has implicit queuing, but we are
+ # in a task callback here so should be calling transport send directly
+ transport.send(message)
+ end
+ end
+end
diff --git a/drivers/panasonic/projector/nt_control_spec.cr b/drivers/panasonic/projector/nt_control_spec.cr
new file mode 100644
index 00000000000..dc26608fc1a
--- /dev/null
+++ b/drivers/panasonic/projector/nt_control_spec.cr
@@ -0,0 +1,23 @@
+DriverSpecs.mock_driver "Panasonic::Projector::NTControl" do
+ # Execute a command (triggers the connection)
+ exec(:power?)
+
+ # Once connected the projector will send a password salt
+ expect_reconnect
+ transmit "NTCONTROL 1 09b075be\r"
+
+ # Check the request was sent with the correct password
+ password = "d4a58eaea919558fb54a33a2effa8b94"
+ should_send("#{password}00Q$S\r")
+
+ # Respond with the status then check the state updated
+ transmit("00PON\r")
+ status[:power].should be_true
+
+ exec(:lamp_hours?)
+ expect_reconnect
+ transmit "NTCONTROL 1 09b075be\r"
+ should_send("#{password}00Q$L:1\r")
+ transmit("001682\r")
+ status[:lamp_usage].should eq(1682)
+end
diff --git a/drivers/place/area_config.cr b/drivers/place/area_config.cr
new file mode 100644
index 00000000000..e2fedc891ed
--- /dev/null
+++ b/drivers/place/area_config.cr
@@ -0,0 +1,67 @@
+require "json"
+require "./area_polygon"
+
+module Place
+ class Geometry
+ include JSON::Serializable
+
+ def initialize(@coordinates, @geo_type = "Polygon")
+ end
+
+ @[JSON::Field(key: "type")]
+ property geo_type : String
+ property coordinates : Array(Tuple(Float64, Float64))
+ end
+
+ class AreaConfig
+ include JSON::Serializable
+
+ def initialize(@id, name, coordinates, building_id = nil, @area_type = "Feature", @feature_type = "section")
+ @geometry = Geometry.new(coordinates)
+ @properties = Hash(String, JSON::Any::Type | Hash(String, JSON::Any)).new
+ @properties["name"] = name
+ @properties["building_id"] = building_id if building_id
+ end
+
+ @[JSON::Field(ignore: true)]
+ @polygon : Polygon? = nil
+
+ property id : String
+
+ @[JSON::Field(key: "type")]
+ property area_type : String
+ property feature_type : String
+
+ property geometry : Geometry
+ property properties : Hash(String, JSON::Any::Type)
+
+ @[JSON::Field(ignore: true)]
+ @adjusted_coords : Array(Tuple(Float64, Float64))? = nil
+
+ def name : String
+ self.properties["name"].as(String)
+ end
+
+ def building : String?
+ if id = self.properties["building_id"]?
+ id.as(String)
+ end
+ end
+
+ def coordinates
+ if coords = @adjusted_coords
+ coords
+ else
+ self.geometry.coordinates
+ end
+ end
+
+ def coordinates(map_width : Float64, map_height : Float64)
+ @adjusted_coords = self.geometry.coordinates.map { |(x, y)| {x * map_width, y * map_height} }
+ end
+
+ def polygon : Polygon
+ @polygon ||= Polygon.new(coordinates.map { |coords| Point.new(*coords) })
+ end
+ end
+end
diff --git a/drivers/place/area_management.cr b/drivers/place/area_management.cr
new file mode 100644
index 00000000000..08f4cc195a7
--- /dev/null
+++ b/drivers/place/area_management.cr
@@ -0,0 +1,408 @@
+module Place; end
+
+require "set"
+require "placeos"
+require "./area_config"
+require "./area_polygon"
+
+class Place::AreaManagement < PlaceOS::Driver
+ descriptive_name "PlaceOS Area Management"
+ generic_name :AreaManagement
+ description %(counts trackable objects, such as laptops, in building areas)
+
+ accessor staff_api : StaffAPI_1
+
+ default_settings({
+ building: "zone-12345",
+
+ # time in seconds
+ poll_rate: 60,
+
+ # How many wireless devices should we ignore
+ duplication_factor: 0.8,
+
+ # Driver to query
+ location_service: "LocationServices",
+
+ areas: {
+ "zone-1234" => [
+ {
+ id: "lobby1",
+ name: "George St Lobby",
+ building: "building-zone-id",
+ coordinates: [{3, 5}, {5, 6}, {6, 1}],
+ },
+ ],
+ },
+ })
+
+ alias AreaSetting = NamedTuple(
+ id: String,
+ name: String,
+ building: String?,
+ coordinates: Array(Tuple(Float64, Float64)))
+
+ alias AreaDetails = NamedTuple(
+ area_id: String,
+ name: String,
+ count: Int32,
+ )
+
+ alias LevelCapacity = NamedTuple(
+ total_desks: Int32,
+ total_capacity: Int32,
+ desk_ids: Array(String),
+ )
+
+ alias RawLevelDetails = NamedTuple(
+ wireless_devices: Int32,
+ desk_usage: Int32,
+ capacity: LevelCapacity,
+ )
+
+ # zone_id => areas
+ @level_areas : Hash(String, Array(AreaConfig)) = {} of String => Array(AreaConfig)
+ # area_id => area
+ @areas : Hash(String, AreaConfig) = {} of String => AreaConfig
+
+ # zone_id => desk_ids
+ @duplication_factor : Float64 = 0.8
+ @level_details : Hash(String, LevelCapacity) = {} of String => LevelCapacity
+
+ # PlaceOS client config
+ @building_id : String = ""
+
+ @poll_rate : Time::Span = 60.seconds
+ @location_service : String = "LocationServices"
+
+ @rate_limit : Channel(Nil) = Channel(Nil).new
+ @update_lock : Mutex = Mutex.new
+ @terminated = false
+
+ def on_load
+ spawn { rate_limiter }
+ spawn(same_thread: true) { update_scheduler }
+
+ on_update
+ end
+
+ def on_unload
+ @terminated = true
+ @rate_limit.close
+ end
+
+ def on_update
+ @building_id = setting(String, :building)
+
+ @poll_rate = (setting?(Int32, :poll_rate) || 60).seconds
+ @location_service = setting?(String, :location_service).presence || "LocationServices"
+ @duplication_factor = setting?(Float64, :duplication_factor) || 0.8
+
+ # Areas are defined in metadata, this is mainly here so we can write specs
+ if building_areas = setting?(Hash(String, Array(AreaSetting)), :areas)
+ @level_areas.clear
+ building_areas.each do |zone_id, areas|
+ @level_areas[zone_id] = areas.map do |area|
+ config = AreaConfig.new(area[:id], area[:name], area[:coordinates], area[:building])
+ @areas[config.id] = config
+ config
+ end
+ end
+ end
+
+ schedule.clear
+ schedule.every(@poll_rate) { synchronize_all_levels }
+ end
+
+ # The location services provider
+ protected def location_service
+ system[@location_service]
+ end
+
+ # Updates a single zone, syncing the metadata
+ protected def update_level_details(level_details, zone, metadata)
+ return unless zone.tags.includes?("level")
+
+ if desks = metadata["desks"]?
+ ids = desks.details.as_a.map { |desk| desk["id"].as_s }
+ level_details[zone.id] = {
+ total_desks: ids.size,
+ total_capacity: zone.capacity,
+ desk_ids: ids,
+ }
+ else
+ level_details[zone.id] = {
+ total_desks: zone.count,
+ total_capacity: zone.capacity,
+ desk_ids: [] of String,
+ }
+ end
+
+ if regions = metadata["map_regions"]?
+ area_data = Array(AreaConfig).from_json(regions.details["areas"].to_json)
+ @level_areas[zone.id] = area_data
+ area_data.each { |area| @areas[area.id] = area }
+ else
+ @level_areas.delete(zone.id)
+ end
+ end
+
+ alias Zone = PlaceOS::Client::API::Models::Zone
+ alias Metadata = Hash(String, PlaceOS::Client::API::Models::Metadata)
+ alias ChildMetadata = Array(NamedTuple(zone: Zone, metadata: Metadata))
+
+ # Grabs all the level zones in the building and syncs the metadata
+ protected def sync_level_details
+ # Attempt to obtain the latest version of the metadata
+ response = ChildMetadata.from_json(staff_api.metadata_children(@building_id).get.to_json)
+
+ level_details = {} of String => LevelCapacity
+ response.each do |meta|
+ update_level_details(level_details, meta[:zone], meta[:metadata])
+ end
+ @level_details = level_details
+ rescue error
+ logger.error(exception: error) { "obtaining level metadata" }
+ end
+
+ protected def update_level_locations(level_counts, level_id, details)
+ areas = @level_areas[level_id]? || [] of AreaConfig
+
+ # Provide the frontend with the list of all known desk ids on a level
+ self["#{level_id}:desk_ids"] = details[:desk_ids]
+
+ # Get location data for the level
+ locations = location_service.device_locations(level_id).get.as_a
+
+ # Provide to the frontend
+ self[level_id] = {
+ value: locations,
+ ts_hint: "complex",
+ ts_map: {
+ x: "xloc",
+ y: "yloc",
+ },
+ ts_tag_keys: {"s2_cell_id"},
+ ts_tags: {
+ pos_building: @building_id,
+ pos_level: level_id,
+ },
+ }
+
+ # Grab the x,y locations
+ wireless_count = 0
+ desk_count = 0
+ xy_locs = locations.select do |loc|
+ case loc["location"].as_s
+ when "wireless"
+ wireless_count += 1
+
+ # Keep if x, y coords are present
+ !loc["x"].raw.nil?
+ when "desk"
+ desk_count += 1
+ false
+ else
+ false
+ end
+ end
+
+ # build the level overview
+ level_counts[level_id] = {
+ wireless_devices: wireless_count,
+ desk_usage: desk_count,
+ capacity: details,
+ }
+
+ # we need to know the map dimensions to be able to count people in areas
+ map_width = -1.0
+ map_height = -1.0
+
+ if tmp_loc = xy_locs[0]?
+ # ensure map width and height are known
+ map_width_raw = tmp_loc["map_width"]?.try(&.raw)
+ case map_width_raw
+ when Int64, Float64
+ map_width = map_width_raw.to_f
+ end
+
+ map_height_raw = tmp_loc["map_height"]?.try(&.raw)
+ case map_height_raw
+ when Int64, Float64
+ map_height = map_height_raw.to_f
+ end
+ end
+
+ # Calculate the device counts for each area
+ area_counts = [] of AreaDetails
+ if map_width && map_height
+ areas.each do |area|
+ count = 0
+
+ # Ensure the area is configured
+ area.coordinates(map_width, map_height)
+ polygon = area.polygon
+
+ # Calculate counts, our config uses browser coordinate systems,
+ # so need to adjust any x,y values being received for this
+ xy_locs.each do |loc|
+ case loc["coordinates_from"]?.try(&.raw)
+ when "bottom-left"
+ count += 1 if polygon.contains(loc["x"].as_f, map_height - loc["y"].as_f)
+ else
+ count += 1 if polygon.contains(loc["x"].as_f, loc["y"].as_f)
+ end
+ end
+
+ area_counts << {
+ area_id: area.id,
+ name: area.name,
+ count: count,
+ }
+ end
+ end
+
+ # Provide the frontend the area details
+ self["#{level_id}:areas"] = {
+ value: area_counts,
+ ts_hint: "complex",
+ ts_fields: {
+ pos_level: level_id,
+ },
+ ts_tags: {
+ pos_building: @building_id,
+ },
+ }
+ rescue error
+ log_location_parsing(error, level_id)
+ sleep 200.milliseconds
+ end
+
+ @level_counts : Hash(String, RawLevelDetails) = {} of String => RawLevelDetails
+
+ def request_locations
+ @update_lock.synchronize do
+ sync_level_details
+
+ # level => user count
+ level_counts = {} of String => RawLevelDetails
+ @level_details.each do |level_id, details|
+ update_level_locations(level_counts, level_id, details)
+ end
+ @level_counts = level_counts
+ update_overview
+ end
+ end
+
+ def request_level_locations(level_id : String) : Nil
+ @update_lock.synchronize do
+ zone = Zone.from_json(staff_api.zone(level_id).get.to_json)
+ if !zone.tags.includes?("level")
+ logger.warn { "attempted to update location for #{zone.name} (#{level_id}) which is not tagged as a level" }
+ return
+ end
+ metadata = Metadata.from_json(staff_api.metadata(level_id).get.to_json)
+
+ update_level_details @level_details, zone, metadata
+ update_level_locations @level_counts, level_id, @level_details[level_id]
+ update_overview
+ end
+ end
+
+ protected def update_overview
+ self[:overview] = @level_counts.transform_values { |details| build_level_stats(**details) }
+ end
+
+ protected def log_location_parsing(error, level_id)
+ logger.debug(exception: error) { "while parsing #{level_id}" }
+ rescue
+ end
+
+ def is_inside?(x : Float64, y : Float64, area_id : String)
+ area = @areas[area_id]
+ area.polygon.contains(x, y)
+ end
+
+ protected def build_level_stats(wireless_devices, desk_usage, capacity)
+ # raw data
+ total_desks = capacity[:total_desks]
+ total_capacity = capacity[:total_capacity]
+
+ # normalised data
+ adjusted_devices = wireless_devices * @duplication_factor
+
+ if total_capacity <= 0
+ percentage_use = 100.0
+ individual_impact = 100.0
+ else
+ percentage_use = (adjusted_devices / total_capacity) * 100.0
+ individual_impact = 100.0 / total_capacity
+ end
+ remaining_capacity = total_capacity - adjusted_devices
+ recommendation = remaining_capacity + remaining_capacity * individual_impact
+
+ {
+ desk_count: total_desks,
+ desk_usage: desk_usage,
+ device_capacity: total_capacity,
+ device_count: wireless_devices,
+ estimated_people: adjusted_devices.to_i,
+ percentage_use: percentage_use,
+
+ # higher the number, better the recommendation
+ recommendation: recommendation,
+ }
+ end
+
+ # This is to limit the number of "real-time" updates
+ # batching operations to provide fast updates that don't waste CPU cycles
+ protected def rate_limiter
+ sleep 3
+
+ loop do
+ begin
+ break if @rate_limit.closed?
+ @rate_limit.send(nil)
+ rescue error
+ logger.error(exception: error) { "issue with rate limiter" }
+ ensure
+ sleep 3
+ end
+ end
+ rescue
+ # Possible error with logging exception, restart rate limiter silently
+ spawn { rate_limiter } unless @terminated
+ end
+
+ @update_levels : Set(String) = Set.new([] of String)
+ @update_all : Bool = true
+ @schedule_lock : Mutex = Mutex.new
+
+ def update_available(level_ids : Array(String))
+ @schedule_lock.synchronize { @update_levels.concat level_ids }
+ end
+
+ def synchronize_all_levels
+ @schedule_lock.synchronize { @update_all = true }
+ end
+
+ protected def update_scheduler
+ loop do
+ @rate_limit.receive
+ @schedule_lock.synchronize do
+ begin
+ if @update_all
+ request_locations
+ else
+ @update_levels.each { |level_id| request_level_locations level_id }
+ end
+ rescue error
+ logger.error(exception: error) { "error updating floors" }
+ ensure
+ @update_levels.clear
+ @update_all = false
+ end
+ end
+ end
+ end
+end
diff --git a/drivers/place/area_management_spec.cr b/drivers/place/area_management_spec.cr
new file mode 100644
index 00000000000..5b38a1457c1
--- /dev/null
+++ b/drivers/place/area_management_spec.cr
@@ -0,0 +1,16 @@
+DriverSpecs.mock_driver "Place::AreaCount" do
+ # Used this tool to work out coordinates: https://www.mathsisfun.com/geometry/polygons-interactive.html
+ exec(:is_inside?, 4, 5, "lobby1").get.should eq(true)
+ exec(:is_inside?, 4, 4, "lobby1").get.should eq(true)
+ exec(:is_inside?, 5, 5, "lobby1").get.should eq(true)
+ exec(:is_inside?, 5, 4, "lobby1").get.should eq(true)
+ exec(:is_inside?, 5, 3, "lobby1").get.should eq(true)
+ exec(:is_inside?, 3.1, 5, "lobby1").get.should eq(true)
+
+ exec(:is_inside?, 3, 6, "lobby1").get.should eq(false)
+ exec(:is_inside?, 4, 6, "lobby1").get.should eq(false)
+ exec(:is_inside?, 4.6, 5.9, "lobby1").get.should eq(false)
+ exec(:is_inside?, 5.2, 5.4, "lobby1").get.should eq(false)
+ exec(:is_inside?, 5.5, 1.5, "lobby1").get.should eq(false)
+ exec(:is_inside?, 5.9, 2, "lobby1").get.should eq(false)
+end
diff --git a/drivers/place/area_polygon.cr b/drivers/place/area_polygon.cr
new file mode 100644
index 00000000000..8bdfe7c349d
--- /dev/null
+++ b/drivers/place/area_polygon.cr
@@ -0,0 +1,60 @@
+# Crystal lang point in a polygon, based on
+# https://wrf.ecse.rpi.edu/Research/Short_Notes/pnpoly.html
+
+# 1. The polygon may contain multiple separate components, and/or holes, which may be concave, provided that you separate the components and holes with a (0,0) vertex, as follows.
+# First, include a (0,0) vertex.
+# Then include the first component' vertices, repeating its first vertex after the last vertex.
+# Include another (0,0) vertex.
+# Include another component or hole, repeating its first vertex after the last vertex.
+# Repeat the above two steps for each component and hole.
+# Include a final (0,0) vertex.
+# 2. For example, let three components' vertices be A1, A2, A3, B1, B2, B3, and C1, C2, C3. Let two holes be H1, H2, H3, and I1, I2, I3. Let O be the point (0,0). List the vertices thus:
+# O, A1, A2, A3, A1, O, B1, B2, B3, B1, O, C1, C2, C3, C1, O, H1, H2, H3, H1, O, I1, I2, I3, I1, O.
+# 3. Each component or hole's vertices may be listed either clockwise or counter-clockwise.
+# 4. If there is only one connected component, then it is optional to repeat the first vertex at the end. It's also optional to surround the component with zero vertices.
+
+struct Point
+ def initialize(@x : Float64, @y : Float64)
+ end
+
+ property x : Float64
+ property y : Float64
+end
+
+class Polygon
+ def initialize(@points : Array(Point))
+ @xmax = @xmin = @points[0].x
+ @ymax = @ymin = @points[0].y
+
+ @points[1..-1].each do |point|
+ @xmax = point.x if point.x > @xmax
+ @ymax = point.y if point.y > @ymax
+ @xmin = point.x if point.x < @xmin
+ @ymin = point.y if point.y < @ymin
+ end
+ end
+
+ getter points : Array(Point)
+ getter xmin : Float64
+ getter ymin : Float64
+ getter xmax : Float64
+ getter ymax : Float64
+
+ def contains(testx : Float64, testy : Float64)
+ # definitely not within the polygon, quick check
+ return false if testx < @xmin || testx > @xmax || testy < @ymin || testy > @ymax
+
+ inside = false
+ previous_index = @points.size - 1
+
+ @points.each_with_index do |point, index|
+ previous = @points[previous_index]
+ if ((point.y > testy) != (previous.y > testy)) && (testx < (previous.x - point.x) * (testy - point.y) / (previous.y - point.y) + point.x)
+ inside = !inside
+ end
+ previous_index = index
+ end
+
+ inside
+ end
+end
diff --git a/drivers/place/bookings.cr b/drivers/place/bookings.cr
new file mode 100644
index 00000000000..a8565f6de8e
--- /dev/null
+++ b/drivers/place/bookings.cr
@@ -0,0 +1,279 @@
+module Place; end
+
+require "place_calendar"
+
+class Place::Bookings < PlaceOS::Driver
+ descriptive_name "PlaceOS Bookings"
+ generic_name :Bookings
+
+ default_settings({
+ calendar_id: nil,
+ calendar_time_zone: "Australia/Sydney",
+ book_now_default_title: "Ad Hoc booking",
+ disable_book_now: false,
+ disable_end_meeting: false,
+ pending_period: 5,
+ pending_from: 5,
+ cache_polling_period: 2,
+
+ control_ui: "https://if.panel/to_be_used_for_control",
+ catering_ui: "https://if.panel/to_be_used_for_catering",
+ })
+
+ accessor calendar : Calendar_1
+
+ @calendar_id : String = ""
+ @time_zone : Time::Location = Time::Location.load("Australia/Sydney")
+ @default_title : String = "Ad Hoc booking"
+ @disable_book_now : Bool = false
+ @disable_end_meeting : Bool = false
+ @pending_period : Time::Span = 5.minutes
+ @pending_before : Time::Span = 5.minutes
+ @bookings : Array(JSON::Any) = [] of JSON::Any
+
+ def on_load
+ monitor("staff/event/changed") { |_subscription, payload| check_change(payload) }
+
+ on_update
+ end
+
+ def on_update
+ schedule.clear
+ @calendar_id = setting?(String, :calendar_id).presence || system.email.not_nil!
+
+ schedule.in(Random.rand(60).seconds + Random.rand(1000).milliseconds) { poll_events }
+
+ cache_polling_period = (setting?(UInt32, :cache_polling_period) || 2_u32).minutes
+ cache_polling_period += Random.rand(30).seconds + Random.rand(1000).milliseconds
+ schedule.every(cache_polling_period) { poll_events }
+
+ time_zone = setting?(String, :calendar_time_zone).presence
+ @time_zone = Time::Location.load(time_zone) if time_zone
+
+ @default_title = setting?(String, :book_now_default_title).presence || "Ad Hoc booking"
+
+ book_now = setting?(Bool, :disable_book_now)
+ @disable_book_now = book_now.nil? ? !system.bookable : !!book_now
+ @disable_end_meeting = !!setting?(Bool, :disable_end_meeting)
+
+ pending_period = setting?(UInt32, :pending_period) || 5_u32
+ @pending_period = pending_period.minutes
+
+ pending_before = setting?(UInt32, :pending_before) || 5_u32
+ @pending_before = pending_before.minutes
+
+ @last_booking_started = setting?(Int64, :last_booking_started) || 0_i64
+
+ # Write to redis last on the off chance there is a connection issue
+ self[:default_title] = @default_title
+ self[:disable_book_now] = @disable_book_now
+ self[:disable_end_meeting] = @disable_end_meeting
+ self[:pending_period] = pending_period
+ self[:pending_before] = pending_before
+ self[:control_ui] = setting?(String, :control_ui)
+ self[:catering_ui] = setting?(String, :catering_ui)
+ end
+
+ # This is how we check the rooms status
+ @last_booking_started : Int64 = 0_i64
+
+ def start_meeting(meeting_start_time : Int64) : Nil
+ logger.debug { "starting meeting #{meeting_start_time}" }
+ @last_booking_started = meeting_start_time
+ define_setting(:last_booking_started, meeting_start_time)
+ check_current_booking
+ end
+
+ # End either the current meeting early, or the pending meeting
+ def end_meeting(meeting_start_time : Int64) : Nil
+ cmeeting = current
+ result = if cmeeting && cmeeting.event_start.to_unix == meeting_start_time
+ logger.debug { "deleting event #{cmeeting.title}, from #{@calendar_id}" }
+ calendar.delete_event(@calendar_id, cmeeting.id)
+ else
+ nmeeting = upcoming
+ if nmeeting && nmeeting.event_start.to_unix == meeting_start_time
+ logger.debug { "deleting event #{nmeeting.title}, from #{@calendar_id}" }
+ calendar.delete_event(@calendar_id, nmeeting.id)
+ else
+ raise "only the current or pending meeting can be cancelled"
+ end
+ end
+ result.get
+
+ # Update the display
+ poll_events
+ check_current_booking
+ end
+
+ def book_now(period_in_seconds : Int64, title : String? = nil, owner : String? = nil)
+ title ||= @default_title
+ starting = Time.utc.to_unix
+ ending = starting + period_in_seconds
+
+ logger.debug { "booking event #{title}, from #{starting}, to #{ending}, in #{@time_zone.name}, on #{@calendar_id}" }
+
+ host_calendar = owner.presence || @calendar_id
+ room_email = system.email.not_nil!
+ room_is_organizer = host_calendar == room_email
+ event = calendar.create_event(
+ title,
+ starting,
+ ending,
+ "",
+ [PlaceCalendar::Event::Attendee.new(room_email, room_email, "accepted", true, room_is_organizer)],
+ @time_zone.name,
+ nil,
+ host_calendar
+ )
+ # Update booking info after creating event
+ poll_events
+ event
+ end
+
+ def poll_events : Nil
+ now = Time.local @time_zone
+ start_of_week = now.at_beginning_of_week.to_unix
+ four_weeks_time = start_of_week + 30.days.to_i
+
+ logger.debug { "polling events #{@calendar_id}, from #{start_of_week}, to #{four_weeks_time}, in #{@time_zone.name}" }
+
+ events = calendar.list_events(
+ @calendar_id,
+ start_of_week,
+ four_weeks_time,
+ @time_zone.name
+ ).get
+
+ @bookings = events.as_a.sort { |a, b| a["event_start"].as_i64 <=> b["event_start"].as_i64 }
+ self[:bookings] = @bookings
+
+ check_current_booking
+ end
+
+ protected def check_current_booking : Nil
+ now = Time.utc.to_unix
+ previous_booking = nil
+ current_booking = nil
+ next_booking = Int32::MAX
+
+ @bookings.each_with_index do |event, index|
+ starting = event["event_start"].as_i64
+
+ # All meetings are in the future
+ if starting > now
+ next_booking = index
+ previous_booking = index - 1 if index > 0
+ break
+ end
+
+ # Calculate event end time
+ ending_unix = if ending = event["event_end"]?
+ ending.as_i64
+ else
+ starting + 24.hours.to_i
+ end
+
+ # Event ended in the past
+ next if ending_unix < now
+
+ # We've found the current event
+ if starting <= now && ending_unix > now
+ current_booking = index
+ previous_booking = index - 1 if index > 0
+ next_booking = index + 1
+ break
+ end
+ end
+
+ self[:previous_booking] = previous_booking ? @bookings[previous_booking] : nil
+
+ # Configure room status (free, pending, in-use)
+ current_pending = false
+ next_pending = false
+ booked = false
+
+ if current_booking
+ booking = @bookings[current_booking]
+ start_time = booking["event_start"].as_i64
+
+ booked = true
+ # Up to the frontend to delete pending bookings that have past their start time
+ if !@disable_end_meeting
+ current_pending = true if start_time > @last_booking_started
+ elsif @pending_period.to_i > 0_i64
+ pending_limit = (Time.unix(start_time) + @pending_period).to_unix
+ current_pending = true if start_time < pending_limit
+ end
+
+ self[:current_booking] = booking
+ else
+ self[:current_booking] = nil
+ end
+
+ self[:booked] = booked
+
+ # We haven't checked the index of `next_booking` exists, hence the `[]?`
+ if booking = @bookings[next_booking]?
+ start_time = booking["event_start"].as_i64
+ next_pending = true if start_time <= @pending_before.from_now.to_unix
+ self[:next_booking] = booking
+ else
+ self[:next_booking] = nil
+ end
+
+ # Check if pending is enabled
+ if @pending_period.to_i > 0_i64
+ self[:current_pending] = current_pending
+ self[:next_pending] = next_pending
+ self[:pending] = current_pending || next_pending
+
+ self[:in_use] = booked && !current_pending
+ else
+ self[:current_pending] = nil
+ self[:next_pending] = nil
+ self[:pending] = nil
+
+ self[:in_use] = booked
+ end
+
+ # TODO:: set video_conference_url if found in the event details
+
+ self[:status] = (current_pending || next_pending) ? "pending" : (booked ? "busy" : "free")
+ end
+
+ protected def current : PlaceCalendar::Event?
+ status?(PlaceCalendar::Event, :current_booking)
+ end
+
+ protected def upcoming : PlaceCalendar::Event?
+ status?(PlaceCalendar::Event, :next_booking)
+ end
+
+ class StaffEventChange
+ include JSON::Serializable
+
+ property action : String # create, update, cancelled
+ property system_id : String # primary calendar effected
+ property event_id : String
+ property resource : String # the resource email that is effected
+ end
+
+ # This is called when bookings are modified via the staff app
+ # it allows us to update the cache faster than via polling alone
+ protected def check_change(payload : String)
+ event = StaffEventChange.from_json(payload)
+ if event.system_id == system.id
+ poll_events
+ check_current_booking
+ else
+ matching = @bookings.select { |b| b["id"] == event.event_id }
+ if matching
+ poll_events
+ check_current_booking
+ end
+ end
+ rescue error
+ logger.error { "processing change event: #{error.inspect_with_backtrace}" }
+ end
+end
diff --git a/drivers/place/bookings_spec.cr b/drivers/place/bookings_spec.cr
new file mode 100644
index 00000000000..b26efae3788
--- /dev/null
+++ b/drivers/place/bookings_spec.cr
@@ -0,0 +1,232 @@
+DriverSpecs.mock_driver "Place::Bookings" do
+ system({
+ Calendar: {Calendar},
+ })
+
+ # Check it calculates state properly
+ exec(:poll_events).get
+ bookings = status[:bookings].as_a
+ bookings.size.should eq(4)
+ status[:booked].should eq(true)
+ status[:in_use].should eq(false)
+ status[:pending].should eq(true)
+ status[:current_pending].should eq(true)
+ status[:next_pending].should eq(false)
+ status[:status].should eq("pending")
+
+ current_start = bookings[0]["event_start"]
+
+ # Start a meeting
+ exec(:start_meeting, current_start).get
+ bookings = status[:bookings].as_a
+ bookings.size.should eq(4)
+ status[:booked].should eq(true)
+ status[:in_use].should eq(true)
+ status[:pending].should eq(false)
+ status[:current_pending].should eq(false)
+ status[:next_pending].should eq(false)
+ status[:status].should eq("busy")
+
+ # End a meeting
+ exec(:end_meeting, current_start).get
+ bookings = status[:bookings].as_a
+ bookings.size.should eq(3)
+
+ status[:booked].should eq(false)
+ status[:in_use].should eq(false)
+ status[:pending].should eq(false)
+ status[:current_pending].should eq(false)
+ status[:next_pending].should eq(false)
+ status[:status].should eq("free")
+end
+
+class Calendar < DriverSpecs::MockDriver
+ def on_load
+ self[:checked_calendar] = nil
+ self[:deleted_event] = nil
+ end
+
+ def delete_event(calendar_id : String, event_id : String, user_id : String? = nil)
+ self[:deleted_event] = {calendar_id, event_id}
+ @events = @events.reject { |event| event["id"] == event_id }
+ nil
+ end
+
+ def list_events(
+ calendar_id : String,
+ period_start : Int64,
+ period_end : Int64,
+ time_zone : String? = nil,
+ user_id : String? = nil
+ )
+ self[:checked_calendar] = calendar_id
+ @events
+ end
+
+ @events : Array(Hash(String, Array(Hash(String, String)) | Bool | Int64 | String | Array(Nil))) = [
+ {
+ "event_start" => 10.minutes.ago.to_unix,
+ "event_end" => 20.minutes.from_now.to_unix,
+ "id" => "2hg6c13j9ko8hiugmuj8n3jtap_20200804T000000Z",
+ "host" => "jeremy@place.nology",
+ "title" => "A Standup",
+ "description" => "",
+ "attendees" => [
+ {
+ "name" => "alexandre@place.nology",
+ "email" => "alexandre@place.nology",
+ },
+ {
+ "name" => "candy@place.nology",
+ "email" => "candy@place.nology",
+ },
+ {
+ "name" => "viv@place.nology",
+ "email" => "viv@place.nology",
+ },
+ {
+ "name" => "steve@place.nology",
+ "email" => "steve@place.nology",
+ },
+ {
+ "name" => "jeremy@place.nology",
+ "email" => "jeremy@place.nology",
+ },
+ ],
+ "private" => false,
+ "recurring" => false,
+ "all_day" => false,
+ "timezone" => "UTC",
+ "attachments" => [] of Nil,
+ },
+ {
+ "event_start" => 40.minutes.from_now.to_unix,
+ "event_end" => 1.hour.from_now.to_unix,
+ "id" => "2hg6c13j9ko8hiugmuj8n3jtap_20200806T000000Z",
+ "host" => "jeremy@place.nology",
+ "title" => "A Standup",
+ "description" => "",
+ "attendees" => [
+ {
+ "name" => "alexandre@place.nology",
+ "email" => "alexandre@place.nology",
+ },
+ {
+ "name" => "candy@place.nology",
+ "email" => "candy@place.nology",
+ },
+ {
+ "name" => "viv@place.nology",
+ "email" => "viv@place.nology",
+ },
+ {
+ "name" => "steve@place.nology",
+ "email" => "steve@place.nology",
+ },
+ {
+ "name" => "jeremy@place.nology",
+ "email" => "jeremy@place.nology",
+ },
+ ],
+ "private" => false,
+ "recurring" => false,
+ "all_day" => false,
+ "timezone" => "UTC",
+ "attachments" => [] of Nil,
+ },
+ {
+ "event_start" => 4.hour.from_now.to_unix,
+ "event_end" => 5.hour.from_now.to_unix,
+ "id" => "0e1f5n6a898n85eo9gsj169kh1_20200806T010000Z",
+ "host" => "shreya@external.com",
+ "title" => "Place weekly catchup",
+ "description" => "",
+ "attendees" => [
+ {
+ "name" => "Michael",
+ "email" => "michael@external.com",
+ },
+ {
+ "name" => "Glenn",
+ "email" => "glenn@external.com",
+ },
+ {
+ "name" => "Shreya",
+ "email" => "shreya@external.com",
+ },
+ {
+ "name" => "jeremy@place.nology",
+ "email" => "jeremy@place.nology",
+ },
+ {
+ "name" => "Lisa",
+ "email" => "lisa@external.com",
+ },
+ {
+ "name" => "Sheshank",
+ "email" => "sheshank@external.com",
+ },
+ {
+ "name" => "steve@place.nology",
+ "email" => "steve@place.nology",
+ },
+ {
+ "name" => "Zinoca",
+ "email" => "zain@external.com",
+ },
+ {
+ "name" => "Aymie",
+ "email" => "aymie@external.com",
+ },
+ ],
+ "private" => false,
+ "recurring" => false,
+ "all_day" => false,
+ "timezone" => "UTC",
+ "attachments" => [] of Nil,
+ },
+ {
+ "event_start" => 10.hours.from_now.to_unix,
+ "event_end" => 11.hours.from_now.to_unix,
+ "id" => "d8n8u5a5u8j45jgm5248ir49qs_20200806T010000Z",
+ "host" => "jeremy@place.nology",
+ "title" => "PlaceOS Standup",
+ "description" => "Regular Standup to discuss Engine2 Development and Product Requirements.",
+ "attendees" => [
+ {
+ "name" => "caspian@place.nology",
+ "email" => "caspian@place.nology",
+ },
+ {
+ "name" => "viv@place.nology",
+ "email" => "viv@place.nology",
+ },
+ {
+ "name" => "Kim",
+ "email" => "kim@place.nology",
+ },
+ {
+ "name" => "William",
+ "email" => "w.le@place.nology",
+ },
+ {
+ "name" => "jeremy@place.nology",
+ "email" => "jeremy@place.nology",
+ },
+ {
+ "name" => "Shane",
+ "email" => "shane@place.nology",
+ },
+ {
+ "name" => "steve@place.nology",
+ "email" => "steve@place.nology",
+ },
+ ],
+ "private" => false,
+ "recurring" => false,
+ "all_day" => false,
+ "timezone" => "UTC",
+ "attachments" => [] of Nil,
+ },
+ ]
+end
diff --git a/drivers/place/calendar.cr b/drivers/place/calendar.cr
new file mode 100644
index 00000000000..07fbcb2545f
--- /dev/null
+++ b/drivers/place/calendar.cr
@@ -0,0 +1,296 @@
+module Place; end
+
+require "place_calendar"
+require "placeos-driver/interface/mailer"
+require "qr-code"
+require "qr-code/export/png"
+
+class Place::Calendar < PlaceOS::Driver
+ include PlaceOS::Driver::Interface::Mailer
+
+ descriptive_name "PlaceOS Calendar"
+ generic_name :Calendar
+
+ uri_base "https://staff.app.api.com"
+
+ default_settings({
+ calendar_service_account: "service_account@email.address",
+ calendar_config: {
+ scopes: ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/admin.directory.user.readonly"],
+ domain: "primary.domain.com",
+ sub: "default.service.account@google.com",
+ issuer: "placeos@organisation.iam.gserviceaccount.com",
+ signing_key: "PEM encoded private key",
+ },
+ calendar_config_office: {
+ _note_: "rename to 'calendar_config' for use",
+ tenant: "",
+ client_id: "",
+ client_secret: "",
+ conference_type: nil, # This can be set to "teamsForBusiness" to add a Teams link to EVERY created Event
+ },
+ rate_limit: 5,
+
+ # defaults to calendar_service_account if not configured
+ mailer_from: "email_or_office_userPrincipalName",
+ email_templates: {visitor: {checkin: {
+ subject: "%{name} has arrived",
+ text: "for your meeting at %{time}",
+ }}},
+ })
+
+ alias GoogleParams = NamedTuple(
+ scopes: String | Array(String),
+ domain: String,
+ sub: String,
+ issuer: String,
+ signing_key: String,
+ )
+
+ alias OfficeParams = NamedTuple(
+ tenant: String,
+ client_id: String,
+ client_secret: String,
+ conference_type: String | Nil,
+ )
+
+ @client : PlaceCalendar::Client? = nil
+ @service_account : String? = nil
+ @client_lock : Mutex = Mutex.new
+ @rate_limit : Int32 = 3
+ @channel : Channel(Nil) = Channel(Nil).new(3)
+
+ @queue_lock : Mutex = Mutex.new
+ @queue_size = 0
+ @wait_time : Time::Span = 300.milliseconds
+
+ @mailer_from : String? = nil
+
+ def on_load
+ @channel = Channel(Nil).new(2)
+ spawn { rate_limiter }
+ on_update
+ end
+
+ def on_update
+ @service_account = setting?(String, :calendar_service_account).presence
+ @rate_limit = setting?(Int32, :rate_limit) || 3
+ @wait_time = 1.second / @rate_limit
+
+ @mailer_from = setting?(String, :mailer_from).presence || @service_account
+ @templates = setting?(Templates, :email_templates) || Templates.new
+
+ @client_lock.synchronize do
+ # Work around crystal limitation of splatting a union
+ @client = begin
+ config = setting(GoogleParams, :calendar_config)
+ PlaceCalendar::Client.new(**config)
+ rescue
+ config = setting(OfficeParams, :calendar_config)
+ PlaceCalendar::Client.new(**config)
+ end
+ end
+ end
+
+ protected def client
+ if (@wait_time * @queue_size) > 10.seconds
+ raise "wait time would be exceeded for API request, #{@queue_size} requests already queued"
+ end
+ @queue_lock.synchronize { @queue_size += 1 }
+ @client_lock.synchronize do
+ @channel.receive
+ @queue_lock.synchronize { @queue_size -= 1 }
+ yield @client.not_nil!
+ end
+ end
+
+ def queue_size
+ @queue_size
+ end
+
+ def generate_svg_qrcode(text : String) : String
+ QRCode.new(text).as_svg
+ end
+
+ def generate_png_qrcode(text : String, size : Int32 = 128) : String
+ Base64.strict_encode QRCode.new(text).as_png(size: size)
+ end
+
+ @[Security(Level::Support)]
+ def send_mail(
+ to : String | Array(String),
+ subject : String,
+ message_plaintext : String? = nil,
+ message_html : String? = nil,
+ resource_attachments : Array(ResourceAttachment) = [] of ResourceAttachment,
+ attachments : Array(Attachment) = [] of Attachment,
+ cc : String | Array(String) = [] of String,
+ bcc : String | Array(String) = [] of String,
+ from : String | Array(String) | Nil = nil
+ )
+ sender = case from
+ in String
+ from
+ in Array(String)
+ from.first? || @mailer_from.not_nil!
+ in Nil
+ @mailer_from.not_nil!
+ end
+
+ logger.debug { "an email was sent from: #{sender}, to: #{to}" }
+
+ client &.calendar.send_mail(
+ sender,
+ to,
+ subject,
+ message_plaintext,
+ message_html,
+ resource_attachments,
+ attachments,
+ cc,
+ bcc
+ )
+ end
+
+ @[Security(Level::Administrator)]
+ def access_token(user_id : String? = nil)
+ logger.info { "access token requested #{user_id}" }
+ client &.access_token(user_id)
+ end
+
+ @[Security(Level::Support)]
+ def get_groups(user_id : String)
+ logger.debug { "getting group membership for user: #{user_id}" }
+ client &.get_groups(user_id)
+ end
+
+ @[Security(Level::Support)]
+ def get_members(group_id : String)
+ logger.debug { "listing members of group: #{group_id}" }
+ client &.get_members(group_id)
+ end
+
+ @[Security(Level::Support)]
+ def list_users(query : String? = nil, limit : Int32? = nil)
+ logger.debug { "listing user details, query #{query}" }
+ client &.list_users(query, limit)
+ end
+
+ @[Security(Level::Support)]
+ def get_user(user_id : String)
+ logger.debug { "getting user details for #{user_id}" }
+ client &.get_user_by_email(user_id)
+ end
+
+ @[Security(Level::Support)]
+ def list_calendars(user_id : String)
+ logger.debug { "listing calendars for #{user_id}" }
+ client &.list_calendars(user_id)
+ end
+
+ # NOTE:: GraphAPI Only!
+ @[Security(Level::Support)]
+ def get_user_manager(user_id : String)
+ logger.debug { "getting manager details for #{user_id}, note: graphAPI only" }
+ client do |_client|
+ if _client.client_id == :office365
+ _client.calendar.as(PlaceCalendar::Office365).client.get_user_manager(user_id).to_place_calendar
+ end
+ end
+ end
+
+ # NOTE:: GraphAPI Only! - here for use with configuration
+ @[Security(Level::Support)]
+ def list_groups(query : String?)
+ logger.debug { "listing groups, filtering by #{query}, note: graphAPI only" }
+ client do |_client|
+ if _client.client_id == :office365
+ _client.calendar.as(PlaceCalendar::Office365).client.list_groups(query)
+ end
+ end
+ end
+
+ # NOTE:: GraphAPI Only!
+ @[Security(Level::Support)]
+ def get_group(group_id : String)
+ logger.debug { "getting group #{group_id}, note: graphAPI only" }
+ client do |_client|
+ if _client.client_id == :office365
+ _client.calendar.as(PlaceCalendar::Office365).client.get_group(group_id)
+ end
+ end
+ end
+
+ @[Security(Level::Support)]
+ def list_events(calendar_id : String, period_start : Int64, period_end : Int64, time_zone : String? = nil, user_id : String? = nil)
+ location = time_zone ? Time::Location.load(time_zone) : Time::Location.local
+ period_start = Time.unix(period_start).in location
+ period_end = Time.unix(period_end).in location
+ user_id = user_id || @service_account.presence || calendar_id
+
+ logger.debug { "listing events for #{calendar_id}" }
+
+ client &.list_events(user_id, calendar_id,
+ period_start: period_start,
+ period_end: period_end
+ )
+ end
+
+ @[Security(Level::Support)]
+ def delete_event(calendar_id : String, event_id : String, user_id : String? = nil)
+ user_id = user_id || @service_account.presence || calendar_id
+
+ logger.debug { "deleting event #{event_id} on #{calendar_id}" }
+
+ client &.delete_event(user_id, event_id, calendar_id: calendar_id)
+ end
+
+ @[Security(Level::Support)]
+ def create_event(
+ title : String,
+ event_start : Int64,
+ event_end : Int64? = nil,
+ description : String = "",
+ attendees : Array(PlaceCalendar::Event::Attendee) = [] of PlaceCalendar::Event::Attendee,
+ location : String? = nil,
+ timezone : String? = nil,
+ user_id : String? = nil,
+ calendar_id : String? = nil
+ )
+ user_id = (user_id || @service_account.presence || calendar_id).not_nil!
+ calendar_id = calendar_id || user_id
+
+ logger.debug { "creating event on #{calendar_id}" }
+
+ event = PlaceCalendar::Event.new
+ event.host = calendar_id
+ event.title = title
+ event.body = description
+ event.location = location
+ event.timezone = timezone
+ event.attendees = attendees
+
+ tz = Time::Location.load(timezone) if timezone
+ event.event_start = timezone ? Time.unix(event_start).in tz.not_nil! : Time.unix(event_start)
+ event.event_end = timezone ? Time.unix(event_end).in tz.not_nil! : Time.unix(event_end) if event_end
+
+ event.all_day = true unless event_end
+
+ client &.create_event(user_id, event, calendar_id)
+ end
+
+ protected def rate_limiter
+ loop do
+ begin
+ @channel.send(nil)
+ rescue error
+ logger.error(exception: error) { "issue with rate limiter" }
+ ensure
+ sleep @wait_time
+ end
+ end
+ rescue
+ # Possible error with logging exception, restart rate limiter silently
+ spawn { rate_limiter }
+ end
+end
diff --git a/drivers/place/desk_booking_webhook.cr b/drivers/place/desk_booking_webhook.cr
new file mode 100644
index 00000000000..c2cb29eff29
--- /dev/null
+++ b/drivers/place/desk_booking_webhook.cr
@@ -0,0 +1,70 @@
+module Place; end
+
+require "http/client"
+
+class Place::DeskBookingWebhook < PlaceOS::Driver
+ descriptive_name "Desk Booking Webhook"
+ generic_name :DeskBookingWebhook
+ description %(sends a webhook with booking information as it changes)
+
+ accessor staff_api : StaffAPI_1
+
+ default_settings({
+ post_uri: "https://remote-server/path",
+ building: "zone-id",
+
+ custom_headers: {
+ "API_KEY" => "123456",
+ },
+
+ # how many days from now do we want to send
+ days_from_now: 14,
+
+ booking_category: "desk",
+
+ debug: false,
+ })
+
+ def on_load
+ monitor("staff/booking/changed") do |_subscription, payload|
+ logger.debug { "received booking changed event #{payload}" }
+ fetch_and_post
+ end
+ schedule.every(24.hours) { fetch_and_post }
+ on_update
+ end
+
+ @time_period : Time::Span = 14.days
+ @booking_category : String = "desk"
+ @custom_headers = {} of String => String
+ @building = ""
+ @post_uri = ""
+ @debug : Bool = false
+
+ def on_update
+ @post_uri = setting(String, :post_uri)
+ @building = setting(String, :building)
+ @custom_headers = setting(Hash(String, String), :custom_headers)
+ @time_period = setting(Int32, :days_from_now).days
+ @booking_category = setting(String, :booking_category)
+ @debug = setting(Bool, :debug)
+
+ fetch_and_post
+ end
+
+ def fetch_and_post
+ period_start = Time.utc.to_unix
+ period_end = @time_period.from_now.to_unix
+ zones = [@building]
+ payload = staff_api.query_bookings(@booking_category, period_start, period_end, zones).get.to_json
+
+ headers = HTTP::Headers.new
+ @custom_headers.each { |key, value| headers[key] = value }
+ headers["Content-Type"] = "application/json; charset=UTF-8"
+
+ logger.debug { "Posting: #{payload} \n with Headers: #{headers}" } if @debug
+ response = HTTP::Client.post @post_uri, headers, body: payload
+ raise "Request failed with #{response.status_code}: #{response.body}" unless response.status_code < 300
+ "#{response.status_code}: #{response.body}"
+ end
+end
diff --git a/drivers/place/desk_bookings_locations.cr b/drivers/place/desk_bookings_locations.cr
new file mode 100644
index 00000000000..a4de0f65448
--- /dev/null
+++ b/drivers/place/desk_bookings_locations.cr
@@ -0,0 +1,230 @@
+module Place; end
+
+require "json"
+require "placeos-driver/interface/locatable"
+
+class Place::DeskBookingsLocations < PlaceOS::Driver
+ include Interface::Locatable
+
+ descriptive_name "PlaceOS Desk Bookings Locations"
+ generic_name :DeskBookings
+ description %(collects desk booking data from the staff API for visualising on a map)
+
+ accessor area_manager : AreaManagement_1
+ accessor staff_api : StaffAPI_1
+
+ default_settings({
+ zone_filter: ["placeos-zone-id"],
+
+ # time in seconds
+ poll_rate: 60,
+ booking_type: "desk",
+ })
+
+ @zone_filter : Array(String) = [] of String
+ @poll_rate : Time::Span = 60.seconds
+ @booking_type : String = "desk"
+
+ def on_load
+ monitor("staff/booking/changed") do |_subscription, payload|
+ logger.debug { "received booking changed event #{payload}" }
+ booking_changed(Booking.from_json(payload))
+ end
+ on_update
+ end
+
+ def on_update
+ @zone_filter = setting?(Array(String), :zone_filter) || [] of String
+ @poll_rate = (setting?(Int32, :poll_rate) || 60).seconds
+
+ @booking_type = setting?(String, :booking_type).presence || "desk"
+
+ map_zones
+ schedule.clear
+ schedule.every(@poll_rate) { query_desk_bookings }
+ schedule.in(5.seconds) { query_desk_bookings }
+ end
+
+ # ===================================
+ # Monitoring desk bookings
+ # ===================================
+ protected def booking_changed(event)
+ return unless event.booking_type == @booking_type
+ matching_zones = @zone_filter & event.zones
+ return if matching_zones.empty?
+
+ logger.debug { "booking event is in a matching zone" }
+
+ case event.action
+ when "create"
+ return unless event.in_progress?
+ # Check if this event is happening now
+ logger.debug { "adding new booking" }
+ @bookings[event.user_email] << event
+ when "cancelled", "rejected"
+ # delete the booking from the levels
+ found = false
+ @bookings[event.user_email].reject! { |booking| found = true if booking.id == event.id }
+ return unless found
+ when "check_in"
+ return unless event.in_progress?
+ @bookings[event.user_email].each { |booking| booking.checked_in = true if booking.id == event.id }
+ when "changed"
+ # Check if this booking is for today and update as required
+ @bookings[event.user_email].reject! { |booking| booking.id == event.id }
+ @bookings[event.user_email] << event if event.in_progress?
+ else
+ # ignore the update (approve)
+ logger.debug { "booking event was ignored" }
+ return
+ end
+
+ area_manager.update_available(matching_zones)
+ end
+
+ # ===================================
+ # Locatable Interface functions
+ # ===================================
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "searching for #{email}, #{username}" }
+ bookings = @bookings[email]? || [] of Booking
+ map_bookings(bookings)
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ logger.debug { "listing MAC addresses assigned to #{email}, #{username}" }
+ found = [] of String
+ @known_users.each { |user_id, (user_email, _name)|
+ found << user_id if email == user_email
+ }
+ found
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ logger.debug { "searching for owner of #{mac_address}" }
+ if user_details = @known_users[mac_address]?
+ email, _name = user_details
+ {
+ location: "booking",
+ assigned_to: email,
+ mac_address: mac_address,
+ }
+ end
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching devices in zone #{zone_id}" }
+ bookings = [] of Booking
+ @bookings.each_value(&.each { |booking|
+ next unless zone_id.in?(booking.zones)
+ bookings << booking
+ })
+ map_bookings(bookings)
+ end
+
+ protected def map_bookings(bookings)
+ bookings.map do |booking|
+ level = nil
+ building = nil
+ booking.zones.each do |zone_id|
+ tags = @zone_mappings[zone_id]
+ level = zone_id if tags.includes? "level"
+ building = zone_id if tags.includes? "building"
+ break if level && building
+ end
+
+ {
+ location: :booking,
+ type: @booking_type,
+ checked_in: booking.checked_in,
+ asset_id: booking.asset_id,
+ booking_id: booking.id,
+ building: building,
+ level: level,
+ ends_at: booking.booking_end,
+ mac: booking.user_id,
+ staff_email: booking.user_email,
+ staff_name: booking.user_name,
+ }
+ end
+ end
+
+ # ===================================
+ # DESK AND ZONE QUERIES
+ # ===================================
+ # zone id => tags
+ @zone_mappings = {} of String => Array(String)
+
+ class ZoneDetails
+ include JSON::Serializable
+ property tags : Array(String)
+ end
+
+ protected def map_zones
+ @zone_mappings = Hash(String, Array(String)).new do |hash, zone_id|
+ # Map zones_ids to tags (level, building etc)
+ hash[zone_id] = staff_api.zone(zone_id).get["tags"].as_a.map(&.as_s)
+ end
+ end
+
+ class Booking
+ include JSON::Serializable
+
+ # This is to support events
+ property action : String?
+
+ property id : Int64
+ property booking_type : String
+ property booking_start : Int64
+ property booking_end : Int64
+ property timezone : String?
+
+ # events use resource_id instead of asset_id
+ property asset_id : String?
+ property resource_id : String?
+
+ def asset_id : String
+ (@asset_id || @resource_id).not_nil!
+ end
+
+ property user_id : String
+ property user_email : String
+ property user_name : String
+
+ property zones : Array(String)
+
+ property checked_in : Bool?
+ property rejected : Bool?
+
+ def in_progress?
+ now = Time.utc.to_unix
+ now >= @booking_start && now < @booking_end
+ end
+ end
+
+ # Email => Array of bookings
+ @bookings : Hash(String, Array(Booking)) = Hash(String, Array(Booking)).new
+
+ # UserID => {Email, Name}
+ @known_users : Hash(String, Tuple(String, String)) = Hash(String, Tuple(String, String)).new
+
+ def query_desk_bookings : Nil
+ bookings = [] of JSON::Any
+ @zone_filter.each { |zone| bookings.concat staff_api.query_bookings(type: @booking_type, zones: {zone}).get.as_a }
+ bookings = bookings.map { |booking| Booking.from_json(booking.to_json) }
+
+ logger.debug { "queried desk bookings, found #{bookings.size}" }
+
+ new_bookings = Hash(String, Array(Booking)).new do |hash, key|
+ hash[key] = [] of Booking
+ end
+
+ bookings.each do |booking|
+ next if booking.rejected
+ new_bookings[booking.user_email] << booking
+ @known_users[booking.user_id] = {booking.user_email, booking.user_name}
+ end
+
+ @bookings = new_bookings
+ end
+end
diff --git a/drivers/place/desk_bookings_locations_spec.cr b/drivers/place/desk_bookings_locations_spec.cr
new file mode 100644
index 00000000000..f99d1459de2
--- /dev/null
+++ b/drivers/place/desk_bookings_locations_spec.cr
@@ -0,0 +1,77 @@
+DriverSpecs.mock_driver "Place::DeskBookingsLocations" do
+ system({
+ StaffAPI: {StaffAPI},
+ AreaManagement: {AreaManagement},
+ })
+
+ now = Time.local
+ start = now.at_beginning_of_day.to_unix
+ ending = now.at_end_of_day.to_unix
+
+ exec(:query_desk_bookings).get
+ resp = exec(:device_locations, "placeos-zone-id").get
+ puts resp
+ resp.should eq([
+ {"location" => "booking", "checked_in" => true, "asset_id" => "desk-123", "booking_id" => 1, "building" => "zone-building", "level" => "placeos-zone-id", "ends_at" => 1610110799, "mac" => "user-1234", "staff_email" => "user1234@org.com", "staff_name" => "Bob Jane"},
+ {"location" => "booking", "checked_in" => false, "asset_id" => "desk-456", "booking_id" => 2, "building" => "zone-building", "level" => "placeos-zone-id", "ends_at" => 1610110799, "mac" => "user-456", "staff_email" => "zdoo@org.com", "staff_name" => "Zee Doo"},
+ ])
+end
+
+class StaffAPI < DriverSpecs::MockDriver
+ def query_bookings(type : String, zones : Array(String))
+ logger.debug { "Querying desk bookings!" }
+
+ now = Time.local
+ start = now.at_beginning_of_day.to_unix
+ ending = now.at_end_of_day.to_unix
+ [{
+ id: 1,
+ booking_type: type,
+ booking_start: start,
+ booking_end: ending,
+ asset_id: "desk-123",
+ user_id: "user-1234",
+ user_email: "user1234@org.com",
+ user_name: "Bob Jane",
+ zones: zones + ["zone-building"],
+ checked_in: true,
+ rejected: false,
+ },
+ {
+ id: 2,
+ booking_type: type,
+ booking_start: start,
+ booking_end: ending,
+ asset_id: "desk-456",
+ user_id: "user-456",
+ user_email: "zdoo@org.com",
+ user_name: "Zee Doo",
+ zones: zones + ["zone-building"],
+ checked_in: false,
+ rejected: false,
+ }]
+ end
+
+ def zone(zone_id : String)
+ logger.info { "requesting zone #{zone_id}" }
+
+ if zone_id == "placeos-zone-id"
+ {
+ id: zone_id,
+ tags: ["level"],
+ }
+ else
+ {
+ id: zone_id,
+ tags: ["building"],
+ }
+ end
+ end
+end
+
+class AreaManagement < DriverSpecs::MockDriver
+ def update_available(zones : Array(String))
+ logger.info { "requested update to #{zones}" }
+ nil
+ end
+end
diff --git a/drivers/place/location_services.cr b/drivers/place/location_services.cr
new file mode 100644
index 00000000000..fc99672a431
--- /dev/null
+++ b/drivers/place/location_services.cr
@@ -0,0 +1,152 @@
+module Place; end
+
+require "json"
+require "placeos-driver/interface/locatable"
+
+class Place::LocationServices < PlaceOS::Driver
+ descriptive_name "PlaceOS Location Services"
+ generic_name :LocationServices
+ description %(collects location data from compatible services and combines the data)
+
+ default_settings({
+ debug_webhook: false,
+
+ # various groups of people one might be interested in contacting
+ emergency_contacts: {
+ "Fire Wardens" => "5542c9f-eaa7-4e74",
+ "First Aid" => "ed9f7608-488f-aeef",
+ },
+ })
+
+ def on_load
+ on_update
+ end
+
+ @debug_webhook : Bool = false
+ @emergency_contacts : Hash(String, String) = {} of String => String
+
+ def on_update
+ @debug_webhook = setting?(Bool, :debug_webhook) || false
+ @emergency_contacts = setting?(Hash(String, String), :emergency_contacts) || Hash(String, String).new
+
+ if !@emergency_contacts.empty?
+ schedule.clear
+ schedule.every(6.hours, immediate: true) { update_contacts_list }
+ end
+ end
+
+ # Runs through all the services that support the Locatable interface
+ # requests location information on the identifier for all of them
+ # concatenates the results and returns them as a single array
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "searching for #{email}, #{username}" }
+ located = [] of JSON::Any
+ system.implementing(Interface::Locatable).locate_user(email, username).get.each do |locations|
+ located.concat locations.as_a
+ end
+ located
+ end
+
+ # Will return an array of MAC address strings
+ # lowercase with no seperation characters abcdeffd1234 etc
+ def macs_assigned_to(email : String? = nil, username : String? = nil)
+ logger.debug { "listing MAC addresses assigned to #{email}, #{username}" }
+ macs = [] of String
+ system.implementing(Interface::Locatable).macs_assigned_to(email, username).get.each do |found|
+ macs.concat found.as_a.map(&.as_s)
+ end
+ macs
+ end
+
+ # Will return `nil` or `{"location": "wireless", "assigned_to": "bob123", "mac_address": "abcd"}`
+ def check_ownership_of(mac_address : String)
+ logger.debug { "searching for owner of #{mac_address}" }
+ owner = nil
+ system.implementing(Interface::Locatable).check_ownership_of(mac_address).get.each do |result|
+ if result != nil
+ owner = result
+ break
+ end
+ end
+ owner
+ end
+
+ # Will return an array of devices and their x, y coordinates
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching devices in zone #{zone_id}" }
+ located = [] of JSON::Any
+ system.implementing(Interface::Locatable).device_locations(zone_id, location).get.each do |locations|
+ located.concat locations.as_a
+ end
+ located
+ end
+
+ # ===============================
+ # IP ADDRESS => MAC ADDRESS
+ # ===============================
+ SUCCESS_RESPONSE = {HTTP::Status::OK, {} of String => String, nil}
+
+ # Webhook handler for accepting IP address to username mappings
+ # This data is typically obtained via domain controller logs
+ def ip_mappings(method : String, headers : Hash(String, Array(String)), body : String)
+ logger.debug { "IP mappings webhook received: #{method},\nheaders #{headers},\nbody size #{body.size}" }
+ logger.debug { body } if @debug_webhook
+
+ # ip, username, domain, hostname
+ ip_map = Array(Tuple(String, String, String, String?)).from_json(body)
+ system.implementing(Interface::Locatable).ip_username_mappings(ip_map)
+
+ SUCCESS_RESPONSE
+ end
+
+ def mac_address_mappings(method : String, headers : Hash(String, Array(String)), body : String)
+ logger.debug { "MAC mappings webhook received: #{method},\nheaders #{headers},\nbody size #{body.size}" }
+ logger.debug { body } if @debug_webhook
+
+ # username, macs, domain
+ username, macs, domain = Tuple(String, Array(String), String?).from_json(body)
+ username = username.strip
+ macs = macs.compact_map do |mac|
+ mac = mac.strip.gsub(/(0x|[^0-9A-Fa-f])*/, "").downcase
+ mac if mac.size == 12
+ end
+ return {HTTP::Status::NOT_ACCEPTABLE, {} of String => String, nil} if username.empty? || macs.empty?
+
+ system.implementing(Interface::Locatable).mac_address_mappings(username, macs, domain)
+
+ SUCCESS_RESPONSE
+ end
+
+ @[Security(Level::Support)]
+ def update_contacts_list
+ if @emergency_contacts.empty?
+ self[:emergency_contacts] = nil
+ return
+ end
+
+ if !system.exists?(:Calendar)
+ logger.warn { "contacts requested however no directory service available" }
+ return
+ end
+
+ directory = system[:Calendar]
+ self[:emergency_contacts] = @emergency_contacts.transform_values { |id|
+ directory.get_members(id).get.as(JSON::Any)
+ }
+ end
+
+ # locates all the of the emergency contacts
+ def locate_contacts(list_name : String)
+ contacts = status(Hash(String, Array(NamedTuple(
+ email: String,
+ username: String))), :emergency_contacts)
+
+ list = contacts[list_name]
+ results = {} of String => Array(JSON::Any)
+ list.each do |person|
+ email = person[:email]
+ results[email] = locate_user(email, person[:username])
+ end
+ results
+ end
+end
diff --git a/drivers/place/location_services_spec.cr b/drivers/place/location_services_spec.cr
new file mode 100644
index 00000000000..fe323d2cfcb
--- /dev/null
+++ b/drivers/place/location_services_spec.cr
@@ -0,0 +1,85 @@
+module Place; end
+
+require "placeos-driver/interface/locatable"
+
+WIRELESS_LOC = {
+ "location" => "wireless",
+ "coordinates_from" => "bottom-left",
+ "x" => 16.764784482481577,
+ "y" => 25.435735950388988,
+ "lng" => 55.274935030154325,
+ "lat" => 25.201036346211698,
+ "variance" => 7.944837533996209,
+ "last_seen" => 1601526474,
+ "building" => "zone_1234",
+ "level" => "zone_1234",
+}
+
+DESK_LOC = {
+ "location" => "desk",
+ "at_location" => true,
+ "map_id" => "desk-4-1006",
+ "building" => "zone_1234",
+ "level" => "zone_1234",
+}
+
+DriverSpecs.mock_driver "Place::LocationServices" do
+ system({
+ Dashboard: {WirelessLocation},
+ DeskManagement: {DeskLocation},
+ })
+
+ exec(:locate_user, "Steve").get.should eq([WIRELESS_LOC, DESK_LOC])
+end
+
+class WirelessLocation < DriverSpecs::MockDriver
+ include PlaceOS::Driver::Interface::Locatable
+
+ def locate_user(email : String? = nil, username : String? = nil)
+ [WIRELESS_LOC]
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ [] of String
+ end
+
+ alias OwnershipMAC = NamedTuple(
+ location: String,
+ assigned_to: String,
+ mac_address: String,
+ )
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ nil
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ nil
+ end
+end
+
+class DeskLocation < DriverSpecs::MockDriver
+ include PlaceOS::Driver::Interface::Locatable
+
+ def locate_user(email : String? = nil, username : String? = nil)
+ [DESK_LOC]
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ [] of String
+ end
+
+ alias OwnershipMAC = NamedTuple(
+ location: String,
+ assigned_to: String,
+ mac_address: String,
+ )
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ nil
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ nil
+ end
+end
diff --git a/drivers/place/logic_example.cr b/drivers/place/logic_example.cr
new file mode 100644
index 00000000000..62eadaebcb8
--- /dev/null
+++ b/drivers/place/logic_example.cr
@@ -0,0 +1,24 @@
+module Place; end
+
+class Place::LogicExample < PlaceOS::Driver
+ descriptive_name "Example Logic"
+ generic_name :ExampleLogic
+
+ accessor main_lcd : Display_1, implementing: Powerable
+
+ def on_update
+ logger.info { "woot! an update #{setting?(String, :name)}" }
+ end
+
+ def power_state?
+ main_lcd[:power]
+ end
+
+ def power(state : Bool)
+ main_lcd.power(state)
+ end
+
+ def display_count
+ system.count(:Display)
+ end
+end
diff --git a/drivers/place/logic_example_spec.cr b/drivers/place/logic_example_spec.cr
new file mode 100644
index 00000000000..0f855966b3b
--- /dev/null
+++ b/drivers/place/logic_example_spec.cr
@@ -0,0 +1,90 @@
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/switchable"
+
+class Display < DriverSpecs::MockDriver
+ include PlaceOS::Driver::Interface::Powerable
+ include PlaceOS::Driver::Interface::Muteable
+
+ enum Inputs
+ HDMI
+ HDMI2
+ VGA
+ VGA2
+ Miracast
+ DVI
+ DisplayPort
+ HDBaseT
+ Composite
+ end
+
+ include PlaceOS::Driver::Interface::InputSelection(Inputs)
+
+ # Configure initial state in on_load
+ def on_load
+ self[:power] = false
+ self[:input] = Inputs::HDMI
+ end
+
+ # implement the abstract methods required by the interfaces
+ def power(state : Bool)
+ self[:power] = state
+ end
+
+ def switch_to(input : Inputs)
+ mute(false)
+ self[:input] = input
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ self[:mute] = state
+ self[:mute0] = state
+ end
+end
+
+class Switcher < DriverSpecs::MockDriver
+ include PlaceOS::Driver::Interface::InputSelection(Int32)
+
+ def switch_to(input : Input)
+ self[:output] = input
+ end
+end
+
+DriverSpecs.mock_driver "Place::LogicExample" do
+ system({
+ Display: {Display, Display},
+ Switcher: {Switcher},
+ })
+
+ exec(:power_state?).get.should eq(false)
+
+ # Should allow updating of settings
+ settings({
+ name: "Steve",
+ })
+
+ # Updating emulated module state
+ system(:Display_1)[:power] = true
+ exec(:power_state?).get.should eq(true)
+
+ # Expecting a function call
+ exec(:power, false)
+ exec(:power_state?).get.should eq(false)
+ system(:Display_1)[:power].should eq(false)
+
+ # Expecting a function call to return a result
+ exec(:power, true).get.should eq(true)
+
+ exec(:display_count).get.should eq(2)
+
+ system({
+ Display: {Display},
+ Switcher: {Switcher},
+ })
+
+ exec(:display_count).get.should eq(1)
+end
diff --git a/drivers/place/mqtt.cr b/drivers/place/mqtt.cr
new file mode 100644
index 00000000000..aba513d4eb6
--- /dev/null
+++ b/drivers/place/mqtt.cr
@@ -0,0 +1,100 @@
+require "./mqtt_transport_adaptor"
+
+class Place::MQTT < PlaceOS::Driver
+ descriptive_name "Generic MQTT"
+ generic_name :GenericMQTT
+
+ tcp_port 1883
+ description %(makes MQTT data available to other drivers in PlaceOS, for use with String payloads)
+
+ default_settings({
+ username: "user",
+ password: "pass",
+ keep_alive: 60,
+ client_id: "placeos",
+ subscriptions: ["root/#"],
+ })
+
+ @keep_alive : Int32 = 60
+ @username : String? = nil
+ @password : String? = nil
+ @client_id : String = "placeos"
+
+ @mqtt : ::MQTT::V3::Client? = nil
+ @subs : Array(String) = [] of String
+ @transport : Place::TransportAdaptor? = nil
+ @sub_proc : Proc(String, Bytes, Nil) = Proc(String, Bytes, Nil).new { |_key, _payload| nil }
+
+ def on_load
+ @sub_proc = Proc(String, Bytes, Nil).new { |key, payload| on_message(key, payload) }
+ on_update
+ end
+
+ def on_update
+ @username = setting?(String, :username)
+ @password = setting?(String, :password)
+ @keep_alive = setting?(Int32, :keep_alive) || 60
+ @client_id = setting?(String, :client_id) || ::MQTT.generate_client_id("placeos_")
+
+ existing = @subs
+ @subs = setting?(Array(String), :subscriptions) || [] of String
+
+ schedule.clear
+ schedule.every((@keep_alive // 3).seconds) { ping }
+
+ if client = @mqtt
+ unsub = existing - @subs
+ newsub = @subs - existing
+
+ unsub.each do |sub|
+ logger.debug { "unsubscribing to #{sub}" }
+ client.unsubscribe(sub)
+ end
+
+ newsub.each do |sub|
+ logger.debug { "subscribing to #{sub}" }
+ client.subscribe(sub, &@sub_proc)
+ end
+ end
+ end
+
+ def connected
+ transp = Place::TransportAdaptor.new(transport, queue)
+ client = ::MQTT::V3::Client.new(transp)
+ @transport = transp
+ @mqtt = client
+
+ logger.debug { "sending connect message" }
+ client.connect(@username, @password, @keep_alive, @client_id)
+ @subs.each do |sub|
+ logger.debug { "subscribing to #{sub}" }
+ client.subscribe(sub, &@sub_proc)
+ end
+ end
+
+ def disconnected
+ @transport = nil
+ @mqtt = nil
+ end
+
+ protected def on_message(key : String, playload : Bytes) : Nil
+ self[key] = String.new(playload)
+ end
+
+ def publish(key : String, payload : String) : Nil
+ logger.debug { "publishing payload to #{key}" }
+ @mqtt.not_nil!.publish(key, payload)
+ nil
+ end
+
+ def ping
+ logger.debug { "sending ping" }
+ @mqtt.not_nil!.ping
+ end
+
+ def received(data, task)
+ logger.debug { "received #{data.size} bytes: 0x#{data.hexstring}" }
+ @transport.try &.process(data)
+ task.try &.success
+ end
+end
diff --git a/drivers/place/mqtt_spec.cr b/drivers/place/mqtt_spec.cr
new file mode 100644
index 00000000000..f6d33f1e8e4
--- /dev/null
+++ b/drivers/place/mqtt_spec.cr
@@ -0,0 +1,70 @@
+require "mqtt"
+
+DriverSpecs.mock_driver "Place::MQTT" do
+ # ============================
+ # CONNECTION
+ # ============================
+ puts "===== CONNECTION NEGOTIATION ====="
+ connect = MQTT::V3::Connect.new
+ connect.id = MQTT::RequestType::Connect
+ connect.keep_alive_seconds = 60_u16
+ connect.client_id = "placeos"
+ connect.clean_start = true
+ connect.username = "user"
+ connect.password = "pass"
+ connect.packet_length = connect.calculate_length
+ should_send(connect.to_slice)
+
+ connack = MQTT::V3::Connack.new
+ connack.id = MQTT::RequestType::Connack
+ connack.packet_length = connack.calculate_length
+ responds(connack.to_slice)
+
+ # ============================
+ # SUBSCRIPTION
+ # ============================
+ puts "===== SUBSCRIPTION REQUESTED ====="
+ packet = MQTT::V3::Subscribe.new
+ packet.id = MQTT::RequestType::Subscribe
+ packet.qos = MQTT::QoS::BrokerReceived
+ packet.message_id = 2_u16
+ packet.topic = "root/#"
+ packet.packet_length = packet.calculate_length
+ should_send(packet.to_slice)
+
+ suback = MQTT::V3::Suback.new
+ suback.id = MQTT::RequestType::Suback
+ suback.message_id = 2_u16
+ suback.return_codes = [MQTT::QoS::FireAndForget]
+ suback.packet_length = suback.calculate_length
+ responds(suback.to_slice)
+
+ # ============================
+ # REMOTE PUBLISH
+ # ============================
+ puts "===== REMOTE PUBLISH ====="
+ publish = MQTT::V3::Publish.new
+ publish.id = MQTT::RequestType::Publish
+ publish.message_id = 3_u16
+ publish.topic = "root/topic"
+ publish.payload = "testing"
+ publish.packet_length = publish.calculate_length
+
+ transmit publish.to_slice
+ sleep 0.1 # wait a bit for processing
+ status["root/topic"].should eq("testing")
+
+ # ============================
+ # DRIVER PUBLISH
+ # ============================
+ puts "===== DRIVER PUBLISH ====="
+ exec(:publish, "root/topic/action", "value")
+
+ publish = MQTT::V3::Publish.new
+ publish.id = MQTT::RequestType::Publish
+ publish.message_id = 3_u16
+ publish.topic = "root/topic/action"
+ publish.payload = "value"
+ publish.packet_length = publish.calculate_length
+ should_send(publish.to_slice)
+end
diff --git a/drivers/place/mqtt_transport_adaptor.cr b/drivers/place/mqtt_transport_adaptor.cr
new file mode 100644
index 00000000000..a3fef439df5
--- /dev/null
+++ b/drivers/place/mqtt_transport_adaptor.cr
@@ -0,0 +1,28 @@
+require "mqtt"
+
+class Place::TransportAdaptor < MQTT::Transport
+ def initialize(@driver, @queue)
+ super()
+ end
+
+ @driver : PlaceOS::Driver::Transport
+ @queue : PlaceOS::Driver::Queue
+
+ def close! : Nil
+ @driver.disconnect
+ end
+
+ def closed? : Bool
+ !@queue.online
+ end
+
+ def send(message) : Nil
+ @driver.send(message)
+ end
+
+ def process(data : Bytes)
+ @tokenizer.extract(data).each do |bytes|
+ spawn { @on_message.try &.call(bytes) }
+ end
+ end
+end
diff --git a/drivers/place/pinger.cr b/drivers/place/pinger.cr
new file mode 100644
index 00000000000..32f14202a00
--- /dev/null
+++ b/drivers/place/pinger.cr
@@ -0,0 +1,43 @@
+module Place; end
+
+require "pinger"
+
+class Place::Pinger < PlaceOS::Driver
+ descriptive_name "Device Pinger"
+ generic_name :Ping
+
+ # Discard port
+ udp_port 9
+ description %(periodically pings a device)
+
+ default_settings({
+ ping_every: 60,
+ })
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ # Use quite a large random value to spread load
+ period = setting?(Int32, :ping_every) || 60
+ period = period * 1000 + rand(1000)
+
+ schedule.clear
+ schedule.every(period.milliseconds) { ping }
+ end
+
+ def ping
+ hostname = config.ip.not_nil!
+ pinger = ::Pinger.new(hostname, count: 3)
+ pinger.ping
+
+ pingable = pinger.pingable
+ if !pingable
+ self[:last_error] = pinger.exception || pinger.warning || "unknown error"
+ end
+
+ set_connected_state pingable
+ self[:pingable] = pingable
+ end
+end
diff --git a/drivers/place/pinger_spec.cr b/drivers/place/pinger_spec.cr
new file mode 100644
index 00000000000..afb2984d37e
--- /dev/null
+++ b/drivers/place/pinger_spec.cr
@@ -0,0 +1,4 @@
+DriverSpecs.mock_driver "Place::Pinger" do
+ exec(:ping).get.should eq(true)
+ status[:pingable].should eq(true)
+end
diff --git a/drivers/place/smtp.cr b/drivers/place/smtp.cr
new file mode 100644
index 00000000000..2bc2226067b
--- /dev/null
+++ b/drivers/place/smtp.cr
@@ -0,0 +1,160 @@
+require "qr-code"
+require "qr-code/export/png"
+require "base64"
+require "email"
+require "uri"
+
+require "placeos-driver/interface/mailer"
+
+class Place::Smtp < PlaceOS::Driver
+ include PlaceOS::Driver::Interface::Mailer
+
+ descriptive_name "SMTP Mailer"
+ generic_name :Mailer
+ uri_base "https://smtp.host.com"
+ description %(sends emails via SMTP)
+
+ default_settings({
+ sender: "support@place.tech",
+ # host: "smtp.host",
+ # port: 587,
+ tls_mode: EMail::Client::TLSMode::STARTTLS.to_s,
+ ssl_verify_ignore: false,
+ username: "", # Username/Password for SMTP servers with basic authorization
+ password: "",
+
+ email_templates: {visitor: {checkin: {
+ subject: "%{name} has arrived",
+ text: "for your meeting at %{time}",
+ }}},
+ })
+
+ private def smtp_client : EMail::Client
+ @smtp_client ||= new_smtp_client
+ end
+
+ @smtp_client : EMail::Client?
+
+ @sender : String = "support@place.tech"
+ @username : String = ""
+ @password : String = ""
+ @host : String = "smtp.host"
+ @port : Int32 = 587
+ @tls_mode : EMail::Client::TLSMode = EMail::Client::TLSMode::STARTTLS
+ @send_lock : Mutex = Mutex.new
+ @ssl_verify_ignore : Bool = false
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ defaults = URI.parse(config.uri.not_nil!)
+ tls_mode = if scheme = defaults.scheme
+ scheme.ends_with?('s') ? EMail::Client::TLSMode::SMTPS : EMail::Client::TLSMode::STARTTLS
+ else
+ EMail::Client::TLSMode::STARTTLS
+ end
+ port = defaults.port || 587
+ host = defaults.host || "smtp.host"
+
+ @username = setting?(String, :username) || ""
+ @password = setting?(String, :password) || ""
+ @sender = setting?(String, :sender) || "support@place.tech"
+ @host = setting?(String, :host) || host
+ @port = setting?(Int32, :port) || port
+ @tls_mode = setting?(EMail::Client::TLSMode, :tls_mode) || tls_mode
+ @ssl_verify_ignore = setting?(Bool, :ssl_verify_ignore) || false
+
+ @smtp_client = new_smtp_client
+
+ @templates = setting?(Templates, :email_templates) || Templates.new
+ end
+
+ # Create and configure an SMTP client
+ private def new_smtp_client
+ email_config = EMail::Client::Config.new(@host, @port)
+ email_config.log = logger
+ email_config.client_name = "PlaceOS"
+
+ unless @username.empty? || @password.empty?
+ email_config.use_auth(@username, @password)
+ end
+
+ email_config.use_tls(@tls_mode)
+ email_config.tls_context.verify_mode = OpenSSL::SSL::VerifyMode::None if @ssl_verify_ignore
+
+ EMail::Client.new(email_config)
+ end
+
+ def generate_svg_qrcode(text : String) : String
+ QRCode.new(text).as_svg
+ end
+
+ def generate_png_qrcode(text : String, size : Int32 = 128) : String
+ Base64.strict_encode QRCode.new(text).as_png(size: size)
+ end
+
+ def send_mail(
+ to : String | Array(String),
+ subject : String,
+ message_plaintext : String? = nil,
+ message_html : String? = nil,
+ resource_attachments : Array(ResourceAttachment) = [] of ResourceAttachment,
+ attachments : Array(Attachment) = [] of Attachment,
+ cc : String | Array(String) = [] of String,
+ bcc : String | Array(String) = [] of String,
+ from : String | Array(String) | Nil = nil
+ ) : Bool
+ to = {to} unless to.is_a?(Array)
+
+ from = {from} unless from.nil? || from.is_a?(Array)
+ cc = {cc} unless cc.nil? || cc.is_a?(Array)
+ bcc = {bcc} unless bcc.nil? || bcc.is_a?(Array)
+
+ message = EMail::Message.new
+
+ message.subject(subject)
+
+ message.sender(@sender)
+
+ if from.nil? || from.empty?
+ message.from(@sender)
+ else
+ from.each { |_from| message.from(_from) }
+ end
+
+ to.each { |_to| message.to(_to) }
+ bcc.each { |_bcc| message.bcc(_bcc) }
+ cc.each { |_cc| message.cc(_cc) }
+
+ message.message(message_plaintext.as(String)) unless message_plaintext.presence.nil?
+ message.message_html(message_html.as(String)) unless message_html.presence.nil?
+
+ # Traverse all attachments
+ {resource_attachments, attachments}.map(&.each).each.flatten.each do |attachment|
+ # Base64 decode to memory, then attach to email
+ attachment_io = IO::Memory.new
+ Base64.decode(attachment[:content], attachment_io)
+ attachment_io.rewind
+
+ case attachment
+ in Attachment
+ message.attach(io: attachment_io, file_name: attachment[:file_name])
+ in ResourceAttachment
+ message.message_resource(io: attachment_io, file_name: attachment[:file_name], cid: attachment[:content_id])
+ end
+ end
+
+ sent = false
+
+ # Ensure only a single send at a time
+ @send_lock.synchronize do
+ smtp_client.start do
+ sent = send(message)
+ end
+ end
+
+ sent
+ end
+end
diff --git a/drivers/place/smtp_spec.cr b/drivers/place/smtp_spec.cr
new file mode 100644
index 00000000000..2d471eb074f
--- /dev/null
+++ b/drivers/place/smtp_spec.cr
@@ -0,0 +1,40 @@
+require "email"
+
+# for local testing use: http://nilhcem.com/FakeSMTP/download.html
+
+DriverSpecs.mock_driver "Place::Smtp" do
+ settings({
+ sender: "support@place.tech",
+ host: ENV["PLACE_SMTP_HOST"]? || "localhost",
+ port: ENV["PLACE_SMTP_PORT"]?.try(&.to_i) || 25,
+ username: ENV["PLACE_SMTP_USER"]? || "", # Username/Password for SMTP servers with basic authorization
+ password: ENV["PLACE_SMTP_PASS"]? || "",
+ tls_mode: ENV["PLACE_SMTP_MODE"]? || "none",
+
+ email_templates: {visitor: {checkin: {
+ subject: "%{name} has arrived",
+ text: "for your meeting at %{time}",
+ }}},
+ })
+
+ response = exec(
+ :send_mail,
+ subject: "Test Email",
+ to: ENV["PLACE_TEST_EMAIL"]? || "support@place.tech",
+ message_plaintext: "Hello!",
+ ).get
+
+ response.should be_true
+
+ response = exec(
+ :send_template,
+ to: "steve@place.tech",
+ template: {"visitor", "checkin"},
+ args: {
+ name: "Bob",
+ time: "1:30pm",
+ }
+ ).get
+
+ response.should be_true
+end
diff --git a/drivers/place/spec_helper.cr b/drivers/place/spec_helper.cr
new file mode 100644
index 00000000000..113fcb6b303
--- /dev/null
+++ b/drivers/place/spec_helper.cr
@@ -0,0 +1,7 @@
+module Place; end
+
+class Place::SpecHelper < PlaceOS::Driver
+ def implemented_in_driver
+ "woot!"
+ end
+end
diff --git a/drivers/place/staff_api.cr b/drivers/place/staff_api.cr
new file mode 100644
index 00000000000..d503fc918dc
--- /dev/null
+++ b/drivers/place/staff_api.cr
@@ -0,0 +1,457 @@
+module Place; end
+
+require "json"
+require "oauth2"
+require "placeos"
+
+class Place::StaffAPI < PlaceOS::Driver
+ descriptive_name "PlaceOS Staff API"
+ generic_name :StaffAPI
+ description %(helpers for requesting data held in the staff API)
+
+ # The PlaceOS API
+ uri_base "https://staff.app.api.com"
+
+ default_settings({
+ # PlaceOS API creds, so we can query the zone metadata
+ username: "",
+ password: "",
+ client_id: "",
+ redirect_uri: "",
+ })
+
+ @place_domain : URI = URI.parse("https://staff.app.api.com")
+ @username : String = ""
+ @password : String = ""
+ @client_id : String = ""
+ @redirect_uri : String = ""
+
+ @running_a_spec : Bool = false
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ # we use the Place Client to query the desk booking data
+ @username = setting(String, :username)
+ @password = setting(String, :password)
+ @client_id = setting(String, :client_id)
+ @redirect_uri = setting(String, :redirect_uri)
+ @place_domain = URI.parse(config.uri.not_nil!)
+
+ @running_a_spec = setting?(Bool, :running_a_spec) || false
+ end
+
+ def get_system(id : String)
+ response = get("/api/engine/v2/systems/#{id}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+
+ raise "unexpected response for system id #{id}: #{response.status_code}\n#{response.body}" unless response.success?
+
+ begin
+ JSON.parse(response.body)
+ rescue error
+ logger.debug { "issue parsing system #{id}:\n#{response.body.inspect}" }
+ raise error
+ end
+ end
+
+ # Staff details returns the information from AD
+ def staff_details(email : String)
+ response = get("/api/staff/v1/people/#{email}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+
+ raise "unexpected response for stafff #{email}: #{response.status_code}\n#{response.body}" unless response.success?
+
+ begin
+ JSON.parse(response.body)
+ rescue error
+ logger.debug { "issue parsing staff #{email}:\n#{response.body.inspect}" }
+ raise error
+ end
+ end
+
+ # ===================================
+ # User details
+ # ===================================
+ def user(id : String)
+ placeos_client.users.fetch(id)
+ end
+
+ @[Security(Level::Support)]
+ def update_user(id : String, body_json : String) : Nil
+ response = patch("/api/engine/v2/users/#{id}", body: body_json, headers: {
+ "Accept" => "application/json",
+ "Content-Type" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+
+ raise "failed to update groups for #{id}: #{response.status_code}" unless response.success?
+ end
+
+ @[Security(Level::Support)]
+ def resource_token
+ response = post("/api/engine/v2/users/resource_token", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+
+ raise "unexpected response #{response.status_code}\n#{response.body}" unless response.success?
+
+ begin
+ JSON.parse(response.body)
+ rescue error
+ logger.debug { "issue parsing:\n#{response.body.inspect}" }
+ raise error
+ end
+ end
+
+ # ===================================
+ # Guest details
+ # ===================================
+ @[Security(Level::Support)]
+ def guest_details(guest_id : String)
+ response = get("/api/staff/v1/guests/#{guest_id}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+
+ raise "unexpected response #{response.status_code}\n#{response.body}" unless response.success?
+
+ begin
+ JSON.parse(response.body)
+ rescue error
+ logger.debug { "issue parsing:\n#{response.body.inspect}" }
+ raise error
+ end
+ end
+
+ @[Security(Level::Support)]
+ def update_guest(id : String, body_json : String) : Nil
+ response = patch("/api/staff/v1/guests/#{id}", body: body_json, headers: {
+ "Accept" => "application/json",
+ "Content-Type" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+
+ raise "failed to update guest #{id}: #{response.status_code}" unless response.success?
+ end
+
+ # ===================================
+ # ZONE METADATA
+ # ===================================
+ def metadata(id : String, key : String? = nil)
+ placeos_client.metadata.fetch(id, key)
+ end
+
+ def metadata_children(id : String, key : String? = nil)
+ placeos_client.metadata.children(id, key)
+ end
+
+ # ===================================
+ # ZONE INFORMATION
+ # ===================================
+ def zone(zone_id : String)
+ placeos_client.zones.fetch zone_id
+ end
+
+ def zones(q : String? = nil,
+ limit : Int32 = 1000,
+ offset : Int32 = 0,
+ parent : String? = nil,
+ tags : Array(String) | String? = nil)
+ placeos_client.zones.search(
+ q: q,
+ limit: limit,
+ offset: offset,
+ parent: parent,
+ tags: tags
+ )
+ end
+
+ # ===================================
+ # BOOKINGS ACTIONS
+ # ===================================
+ @[Security(Level::Support)]
+ def create_booking(
+ booking_type : String,
+ asset_id : String,
+ user_id : String,
+ user_email : String,
+ user_name : String,
+ zones : Array(String),
+ booking_start : Int64? = nil,
+ booking_end : Int64? = nil,
+ checked_in : Bool = false,
+ title : String? = nil,
+ description : String? = nil,
+ time_zone : String? = nil,
+ extension_data : JSON::Any? = nil
+ )
+ now = time_zone ? Time.local(Time::Location.load(time_zone)) : Time.local
+ booking_start ||= now.at_beginning_of_day.to_unix
+ booking_end ||= now.at_end_of_day.to_unix
+
+ logger.debug { "creating a #{booking_type} booking, starting #{booking_start}, asset #{asset_id}" }
+ response = post("/api/staff/v1/bookings", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ }, body: {
+ "booking_start" => booking_start,
+ "booking_end" => booking_end,
+ "booking_type" => booking_type,
+ "asset_id" => asset_id,
+ "user_id" => user_id,
+ "user_email" => user_email,
+ "user_name" => user_name,
+ "zones" => zones,
+ "checked_in" => checked_in,
+ "title" => title,
+ "description" => description,
+ "timezone" => time_zone,
+ "extension_data" => extension_data,
+ }.compact.to_json)
+ raise "issue creating #{booking_type} booking, starting #{booking_start}, asset #{asset_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ @[Security(Level::Support)]
+ def update_booking(
+ booking_id : String | Int64,
+ booking_start : Int64? = nil,
+ booking_end : Int64? = nil,
+ asset_id : String? = nil,
+ title : String? = nil,
+ description : String? = nil,
+ timezone : String? = nil,
+ extension_data : JSON::Any? = nil
+ )
+ logger.debug { "updating booking #{booking_id}" }
+ response = patch("/api/staff/v1/bookings/#{booking_id}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ }, body: {
+ "booking_start" => booking_start,
+ "booking_end" => booking_end,
+ "asset_id" => asset_id,
+ "title" => title,
+ "description" => description,
+ "timezone" => timezone,
+ "extension_data" => extension_data,
+ }.compact.to_json)
+ raise "issue updating booking #{booking_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ @[Security(Level::Support)]
+ def reject(booking_id : String | Int64)
+ logger.debug { "rejecting booking #{booking_id}" }
+ response = post("/api/staff/v1/bookings/#{booking_id}/reject", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue rejecting booking #{booking_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ @[Security(Level::Support)]
+ def approve(booking_id : String | Int64)
+ logger.debug { "approving booking #{booking_id}" }
+ response = post("/api/staff/v1/bookings/#{booking_id}/approve", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue approving booking #{booking_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ @[Security(Level::Support)]
+ def booking_state(booking_id : String | Int64, state : String)
+ logger.debug { "updating booking #{booking_id} state to: #{state}" }
+ response = post("/api/staff/v1/bookings/#{booking_id}/update_state?state=#{state}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue updating booking state #{booking_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ @[Security(Level::Support)]
+ def booking_check_in(booking_id : String | Int64, state : Bool = true)
+ logger.debug { "checking in booking #{booking_id} to: #{state}" }
+ response = post("/api/staff/v1/bookings/#{booking_id}/check_in?state=#{state}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue checking in booking #{booking_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ @[Security(Level::Support)]
+ def booking_delete(booking_id : String | Int64)
+ logger.debug { "deleting booking #{booking_id}" }
+ response = delete("/api/staff/v1/bookings/#{booking_id}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue updating booking state #{booking_id}: #{response.status_code}" unless response.success?
+ true
+ end
+
+ # ===================================
+ # BOOKINGS QUERY
+ # ===================================
+ class Booking
+ include JSON::Serializable
+
+ property id : Int64
+
+ property user_id : String
+ property user_email : String
+ property user_name : String
+ property asset_id : String
+ property zones : Array(String)
+ property booking_type : String
+
+ property booking_start : Int64
+ property booking_end : Int64
+
+ property timezone : String?
+ property title : String?
+ property description : String?
+
+ property checked_in : Bool
+ property rejected : Bool
+ property approved : Bool
+
+ property approver_id : String?
+ property approver_email : String?
+ property approver_name : String?
+
+ property booked_by_id : String
+ property booked_by_email : String
+ property booked_by_name : String
+
+ property process_state : String?
+ property last_changed : Int64?
+ property created : Int64?
+ end
+
+ def query_bookings(
+ type : String,
+ period_start : Int64? = nil,
+ period_end : Int64? = nil,
+ zones : Array(String) = [] of String,
+ user : String? = nil,
+ email : String? = nil,
+ state : String? = nil,
+ created_before : Int64? = nil,
+ created_after : Int64? = nil,
+ approved : Bool? = nil,
+ rejected : Bool? = nil
+ )
+ # Assumes occuring now
+ period_start ||= Time.utc.to_unix
+ period_end ||= 30.minutes.from_now.to_unix
+
+ params = {
+ "period_start" => period_start.to_s,
+ "period_end" => period_end.to_s,
+ "type" => type,
+ }
+ params["zones"] = zones.join(",") unless zones.empty?
+ params["user"] = user if user && !user.empty?
+ params["email"] = email if email && !email.empty?
+ params["state"] = state if state && !state.empty?
+ params["created_before"] = created_before.to_s if created_before
+ params["created_after"] = created_after.to_s if created_after
+ params["approved"] = approved.to_s unless approved.nil?
+ params["rejected"] = rejected.to_s unless rejected.nil?
+
+ # Get the existing bookings from the API to check if there is space
+ response = get("/api/staff/v1/bookings", params, {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue loading list of bookings (zones #{zones}): #{response.status_code}" unless response.success?
+
+ # Just parse it here instead of using the Bookings object
+ # it will be parsed into an object on the far end
+ JSON.parse(response.body)
+ end
+
+ def get_booking(booking_id : String | Int64)
+ logger.debug { "getting booking #{booking_id}" }
+ response = get("/api/staff/v1/bookings/#{booking_id}", headers: {
+ "Accept" => "application/json",
+ "Authorization" => "Bearer #{token}",
+ })
+ raise "issue getting booking #{booking_id}: #{response.status_code}" unless response.success?
+ JSON.parse(response.body)
+ end
+
+ # For accessing PlaceOS APIs
+ protected def placeos_client : PlaceOS::Client
+ PlaceOS::Client.new(
+ @place_domain,
+ token: OAuth2::AccessToken::Bearer.new(token, nil)
+ )
+ end
+
+ # ===================================
+ # PLACEOS AUTHENTICATION:
+ # ===================================
+ @access_token : String = ""
+ @access_expires : Time = Time.unix(0)
+
+ protected def authenticate : String
+ uri = @place_domain
+ host = uri.port ? "#{uri.host}:#{uri.port}" : uri.host.not_nil!
+ origin = "#{uri.scheme}://#{host}"
+
+ # Create oauth client, optionally pass custom URIs if needed,
+ # if the authorize or token URIs are not the standard ones
+ # (they can also be absolute URLs)
+ oauth2_client = OAuth2::Client.new(host, @client_id, "",
+ redirect_uri: @redirect_uri,
+ authorize_uri: "#{origin}/auth/oauth/authorize",
+ token_uri: "#{origin}/auth/oauth/token")
+
+ access_token = oauth2_client.get_access_token_using_resource_owner_credentials(
+ @username,
+ @password,
+ "public"
+ ).as(OAuth2::AccessToken::Bearer)
+
+ @access_expires = (access_token.expires_in.not_nil! - 300).seconds.from_now
+ @access_token = access_token.access_token
+ end
+
+ protected def token : String
+ # Don't perform OAuth if we are testing the driver
+ return "spec-test" if @running_a_spec
+ return @access_token if valid_token?
+ authenticate
+ end
+
+ protected def valid_token?
+ Time.utc < @access_expires
+ end
+end
+
+# Deal with bad SSL certificate
+class OpenSSL::SSL::Context::Client
+ def initialize(method : LibSSL::SSLMethod = Context.default_method)
+ super(method)
+
+ self.verify_mode = OpenSSL::SSL::VerifyMode::NONE
+ {% if compare_versions(LibSSL::OPENSSL_VERSION, "1.0.2") >= 0 %}
+ self.default_verify_param = "ssl_server"
+ {% end %}
+ end
+end
diff --git a/drivers/place/staff_api_spec.cr b/drivers/place/staff_api_spec.cr
new file mode 100644
index 00000000000..7a5930f3f67
--- /dev/null
+++ b/drivers/place/staff_api_spec.cr
@@ -0,0 +1,53 @@
+DriverSpecs.mock_driver "Place::StaffAPI" do
+ settings({
+ # PlaceOS API creds, so we can query the zone metadata
+ username: "",
+ password: "",
+ client_id: "",
+ redirect_uri: "",
+
+ running_a_spec: true,
+ })
+
+ resp = exec(:query_bookings, "desk")
+
+ expect_http_request do |request, response|
+ headers = request.headers
+ if headers["Authorization"]? == "Bearer spec-test"
+ response.status_code = 200
+ response << %([{
+ "id": 1234,
+ "user_id": "user-12345",
+ "user_email": "steve@place.tech",
+ "user_name": "Steve T",
+ "asset_id": "desk-2-12",
+ "zones": ["zone-build1", "zone-level2"],
+ "booking_type": "Steve T",
+ "booking_start": 123456,
+ "booking_end": 12345678,
+ "timezone": "Australia/Sydney",
+ "checked_in": true,
+ "rejected": false,
+ "approved": false
+ }])
+ else
+ response.status_code = 401
+ end
+ end
+
+ resp.get.should eq(JSON.parse(%([{
+ "id": 1234,
+ "user_id": "user-12345",
+ "user_email": "steve@place.tech",
+ "user_name": "Steve T",
+ "asset_id": "desk-2-12",
+ "zones": ["zone-build1", "zone-level2"],
+ "booking_type": "Steve T",
+ "booking_start": 123456,
+ "booking_end": 12345678,
+ "timezone": "Australia/Sydney",
+ "checked_in": true,
+ "rejected": false,
+ "approved": false
+ }])))
+end
diff --git a/drivers/place/user_group_mappings.cr b/drivers/place/user_group_mappings.cr
new file mode 100644
index 00000000000..82058b11525
--- /dev/null
+++ b/drivers/place/user_group_mappings.cr
@@ -0,0 +1,113 @@
+module Place; end
+
+class Place::UserGroupMappings < PlaceOS::Driver
+ descriptive_name "User Group Mappings"
+ generic_name :UserGroupMappings
+ description "monitors user logins and maps relevent groups to the local user profile"
+
+ accessor staff_api : StaffAPI_1
+ accessor calendar_api : Calendar_1
+
+ default_settings({
+ # ID => place_name
+ group_mappings: {
+ "group_id" => {
+ place_id: "manager",
+ description: "managers of the level2 building",
+ },
+ "group2_id" => {
+ place_id: "boss",
+ description: "people that can access everything",
+ },
+ },
+
+ # Group name prefix => group mappings
+ group_prefix: {
+ "group_name_prefix_" => {
+ strip_prefix: false,
+ place_id: "optional-place-id",
+ },
+ },
+ })
+
+ class UserLogin
+ include JSON::Serializable
+
+ property user_id : String
+ property provider : String
+ end
+
+ def on_load
+ monitor("auth/login") { |_subscription, payload| new_user_login(payload) }
+ on_update
+ end
+
+ alias Mapping = NamedTuple(place_id: String)
+ alias Prefix = NamedTuple(strip_prefix: Bool?, place_id: String?)
+
+ @group_mappings : Hash(String, Mapping) = {} of String => Mapping
+ @group_prefixes : Hash(String, Prefix) = {} of String => Prefix
+ @users_checked : UInt64 = 0_u64
+ @error_count : UInt64 = 0_u64
+
+ def on_update
+ @group_mappings = setting?(Hash(String, Mapping), :group_mappings) || {} of String => Mapping
+ @group_prefixes = setting?(Hash(String, Prefix), :group_prefix) || {} of String => Prefix
+ @group_prefixes = @group_prefixes.transform_keys(&.downcase)
+ end
+
+ protected def new_user_login(user_json)
+ user_details = UserLogin.from_json user_json
+ check_user(user_details.user_id)
+
+ @users_checked += 1
+ self[:users_checked] = @users_checked
+ rescue error
+ logger.error { error.inspect_with_backtrace }
+ self[:last_error] = {
+ error: error.message,
+ time: Time.local.to_s,
+ user: user_json,
+ }
+ @error_count += 1
+ self[:error_count] = @error_count
+ end
+
+ @[Security(Level::Support)]
+ def check_user(id : String) : Nil
+ logger.debug { "checking groups of: #{id}" }
+
+ # Loading the existing user info in PlaceOS (we need the users id)
+ user = NamedTuple(email: String, login_name: String?).from_json staff_api.user(id).get.to_json
+ email = user[:login_name].presence || user[:email]
+ logger.debug { "found placeos user info: #{user[:email]}, id #{user[:email]}" }
+
+ # Request user details from GraphAPI or Google
+ users_groups = calendar_api.get_groups(email).get
+ logger.debug { "found user groups: #{users_groups.to_pretty_json}" }
+ users_groups = users_groups.as_a
+
+ users_group_ids = users_groups.map { |group| group["id"].as_s }
+ users_group_names = users_groups.map { |group| group["name"].as_s.downcase }
+
+ # Build the list of placeos groups based on the mappings and update the user model
+ groups = [] of String
+ @group_mappings.each { |group_id, place_group| groups << place_group[:place_id] if users_group_ids.includes? group_id }
+ @group_prefixes.each do |group_prefix, place_group|
+ users_group_names.each do |name|
+ if name.starts_with?(group_prefix)
+ if place_name = place_group[:place_id]
+ groups << place_name
+ elsif place_group[:strip_prefix]
+ groups << name.split(group_prefix, 2)[-1]
+ else
+ groups << name
+ end
+ end
+ end
+ end
+ staff_api.update_user(id, {groups: groups}.to_json).get
+
+ logger.debug { "checked #{users_groups.size}, found #{groups.size} matching: #{groups}" }
+ end
+end
diff --git a/drivers/place/user_group_mappings_spec.cr b/drivers/place/user_group_mappings_spec.cr
new file mode 100644
index 00000000000..bea0f16a01f
--- /dev/null
+++ b/drivers/place/user_group_mappings_spec.cr
@@ -0,0 +1,43 @@
+class StaffAPI < DriverSpecs::MockDriver
+ def user(id : String)
+ {email: "steve@placeos.tech"}
+ end
+
+ def update_user(id : String, body_json : String) : Nil
+ self[id] = body_json
+ end
+end
+
+class Calendar < DriverSpecs::MockDriver
+ def get_groups(user_id : String)
+ [
+ {
+ id: "5f4694-96f3-4209-a432-b04ac06ca7",
+ name: "Azure-Global-Microsoft Intune Users-Licensed",
+ description: "Azure-Global-Microsoft Intune Users-Licensed",
+ },
+ {
+ id: "bb8836-5942-402d-8d67-55b1a642",
+ name: "All Users",
+ description: "Auto generated group, do not change",
+ },
+ ]
+ end
+end
+
+DriverSpecs.mock_driver "Place::LogicExample" do
+ system({
+ StaffAPI: {StaffAPI},
+ Calendar: {Calendar},
+ })
+
+ settings({
+ group_mappings: {
+ "5f4694-96f3-4209-a432-b04ac06ca7" => {"place_id" => "intune"},
+ "admins" => {"place_id" => "im an admin"},
+ },
+ })
+
+ exec(:check_user, "user-1234").get
+ system(:StaffAPI_1)["user-1234"].should eq({"groups" => ["intune"]}.to_json)
+end
diff --git a/drivers/place/visitor_mailer.cr b/drivers/place/visitor_mailer.cr
new file mode 100644
index 00000000000..328fd4aeacc
--- /dev/null
+++ b/drivers/place/visitor_mailer.cr
@@ -0,0 +1,178 @@
+module Place; end
+
+require "uuid"
+require "oauth2"
+require "placeos-driver/interface/mailer"
+
+class Place::VisitorMailer < PlaceOS::Driver
+ descriptive_name "PlaceOS Visitor Mailer"
+ generic_name :VisitorMailer
+ description %(emails visitors when they are invited and notifies hosts when they check in)
+
+ default_settings({
+ timezone: "GMT",
+ date_time_format: "%c",
+ time_format: "%l:%M%p",
+ date_format: "%A, %-d %B",
+ })
+
+ accessor mailer : Mailer_1, implementing: PlaceOS::Driver::Interface::Mailer
+ accessor staff_api : StaffAPI_1
+
+ def on_load
+ # Guest has been marked as attending a meeting in person
+ monitor("staff/guest/attending") { |_subscription, payload| guest_event(payload.gsub(/[^[:print:]]/, "")) }
+
+ # Guest has arrived in the lobby
+ monitor("staff/guest/checkin") { |_subscription, payload| guest_event(payload.gsub(/[^[:print:]]/, "")) }
+
+ on_update
+ end
+
+ @uri : URI? = nil
+ @host : String = ""
+ @origin : String = ""
+ @time_zone : Time::Location = Time::Location.load("GMT")
+
+ @users_checked_in : UInt64 = 0_u64
+ @error_count : UInt64 = 0_u64
+
+ @visitor_emails_sent : UInt64 = 0_u64
+ @visitor_email_errors : UInt64 = 0_u64
+
+ # See: https://crystal-lang.org/api/0.35.1/Time/Format.html
+ @date_time_format : String = "%c"
+ @time_format : String = "%l:%M%p"
+ @date_format : String = "%A, %-d %B"
+
+ def on_update
+ @date_time_format = setting?(String, :date_time_format) || "%c"
+ @time_format = setting?(String, :time_format) || "%l:%M%p"
+ @date_format = setting?(String, :date_format) || "%A, %-d %B"
+
+ uri = URI.parse(config.uri.not_nil!)
+ @host = uri.port ? "#{uri.host}:#{uri.port}" : uri.host.not_nil!
+ @origin = "#{uri.scheme}://#{@host}"
+ @uri = uri
+
+ time_zone = setting?(String, :calendar_time_zone).presence || "GMT"
+ @time_zone = Time::Location.load(time_zone)
+ end
+
+ class GuestEvent
+ include JSON::Serializable
+
+ property action : String
+ property checkin : Bool?
+ property system_id : String
+ property event_id : String
+ property host : String
+ property resource : String
+ property event_summary : String
+ property event_starting : Int64
+ property attendee_name : String
+ property attendee_email : String
+ property ext_data : Hash(String, JSON::Any)?
+ end
+
+ protected def guest_event(payload)
+ logger.debug { "received guest event payload: #{payload}" }
+ guest_details = GuestEvent.from_json payload
+
+ if guest_details.action == "checkin"
+ # send_checkedin_email(
+ # guest_details.host,
+ # guest_details.attendee_name,
+ # )
+ # self[:users_checked_in] = @users_checked_in += 1
+ else
+ send_visitor_qr_email(
+ guest_details.attendee_email,
+ guest_details.attendee_name,
+ guest_details.host,
+ guest_details.event_id,
+ # guest_details.event_title,
+ guest_details.event_starting,
+ guest_details.system_id,
+ )
+ end
+ rescue error
+ logger.error { error.inspect_with_backtrace }
+ self[:error_count] = @error_count += 1
+ self[:last_error] = {
+ error: error.message,
+ time: Time.local.to_s,
+ user: payload,
+ }
+ end
+
+ @[Security(Level::Support)]
+ def send_visitor_qr_email(
+ visitor_email : String,
+ visitor_name : String,
+ host_email : String,
+ event_id : String,
+ # event_title : String,
+ event_start : Int64,
+ system_id : String
+ )
+ room = get_room_details(system_id)
+
+ local_start_time = Time.unix(event_start).in(@time_zone)
+
+ qr_png = mailer.generate_png_qrcode(text: "VISIT:#{visitor_email},#{system_id},#{event_id}", size: 256).get.as_s
+
+ mailer.send_template(
+ visitor_email,
+ {"visitor_invited", "visitor"}, # Template selection: "visitor_invited" action, "visitor" email
+ {
+ visitor_email: visitor_email,
+ visitor_name: visitor_name,
+ host_name: get_host_name(host_email),
+ room_name: room.display_name.presence || room.name,
+ # event_title: event_title,
+ event_start: local_start_time.to_s(@time_format),
+ event_date: local_start_time.to_s(@date_format),
+ },
+ [
+ {
+ file_name: "qr.png",
+ content: qr_png,
+ content_id: visitor_email,
+ },
+ ]
+ )
+ end
+
+ # ===================================
+ # PlaceOS API requests
+ # ===================================
+
+ class SystemDetails
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+ property display_name : String?
+ property map_id : String?
+ end
+
+ protected def get_room_details(system_id : String, retries = 0)
+ SystemDetails.from_json staff_api.get_system(system_id).get.to_json
+ rescue error
+ raise "issue loading system details #{system_id}" if retries > 3
+ sleep 1
+ get_room_details(system_id, retries + 1)
+ end
+
+ protected def get_host_name(host_email, retries = 0)
+ staff_api.staff_details(host_email).get["name"].as_s.split('(')[0]
+ rescue error
+ if retries > 3
+ logger.error { "issue loading host details #{host_email}" }
+ return "your host"
+ end
+ sleep 1
+ get_host_name(host_email, retries + 1)
+ end
+end
diff --git a/drivers/point_grab/cogni_point.cr b/drivers/point_grab/cogni_point.cr
new file mode 100644
index 00000000000..0f7737bfbd6
--- /dev/null
+++ b/drivers/point_grab/cogni_point.cr
@@ -0,0 +1,385 @@
+require "uri"
+require "uuid"
+
+module PointGrab; end
+
+# Documentation: https://aca.im/driver_docs/PointGrab/CogniPointAPI2-1.pdf
+
+class PointGrab::CogniPoint < PlaceOS::Driver
+ # Discovery Information
+ generic_name :CogniPoint
+ descriptive_name "PointGrab CogniPoint REST API"
+
+ default_settings({
+ user_id: "10000000",
+ app_key: "c5a6adc6-UUID-46e8-b72d-91395bce9565",
+ })
+
+ @user_id : String = ""
+ @app_key : String = ""
+ @auth_token : String = ""
+ @auth_expiry : Time = 1.minute.ago
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @user_id = setting(String, :user_id)
+ @app_key = setting(String, :app_key)
+ end
+
+ class TokenResponse
+ include JSON::Serializable
+
+ property token : String
+ property expires_in : Int32
+ end
+
+ def expire_token!
+ @auth_expiry = 1.minute.ago
+ end
+
+ def token_expired?
+ @auth_expiry < Time.utc
+ end
+
+ def get_token
+ return @auth_token unless token_expired?
+
+ response = post("/be/cp/oauth2/token", body: "grant_type=client_credentials", headers: {
+ "Content-Type" => "application/x-www-form-urlencoded",
+ "Accept" => "application/json",
+ "Authorization" => "Basic #{Base64.strict_encode("#{@user_id}:#{@app_key}")}",
+ })
+
+ body = response.body
+ logger.debug { "received login response: #{body}" }
+
+ if response.success?
+ resp = TokenResponse.from_json(body.not_nil!)
+ token = resp.token
+ @auth_expiry = Time.utc + (resp.expires_in - 5).seconds
+ @auth_token = "Bearer #{resp.token}"
+ else
+ logger.error { "authentication failed with HTTP #{response.status_code}" }
+ raise "failed to obtain access token"
+ end
+ end
+
+ macro get_request(path, result_type)
+ begin
+ %token = get_token
+ %response = get({{path}}, headers: {
+ "Accept" => "application/json",
+ "Authorization" => %token
+ })
+
+ if %response.success?
+ {{result_type}}.from_json(%response.body.not_nil!)
+ else
+ expire_token! if %response.status_code == 401
+ raise "unexpected response #{%response.status_code}\n#{%response.body}"
+ end
+ end
+ end
+
+ class Customer
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+ end
+
+ def customers
+ customers = get_request("/be/cp/v2/customers", NamedTuple(endCustomers: Array(Customer)))
+ customers[:endCustomers]
+ end
+
+ class GeoPosition
+ include JSON::Serializable
+
+ property latitude : Float64
+ property longitude : Float64
+ end
+
+ class MetricPositions
+ include JSON::Serializable
+
+ @[JSON::Field(key: "posX")]
+ property pos_x : Float64
+
+ @[JSON::Field(key: "posY")]
+ property pos_y : Float64
+ end
+
+ class Site
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+
+ class Location
+ include JSON::Serializable
+
+ @[JSON::Field(key: "houseNo")]
+ property house_number : String
+ property street : String
+ property city : String
+ property county : String
+ property state : String
+ property country : String
+ property zip : String
+
+ @[JSON::Field(key: "geoPosition")]
+ property geo_position : GeoPosition
+ end
+
+ @[JSON::Field(key: "customerId")]
+ property customer_id : String
+ property location : Location
+ end
+
+ def sites
+ sites = get_request("/be/cp/v2/sites", NamedTuple(sites: Array(Site)))
+ sites[:sites]
+ end
+
+ def site(site_id : String)
+ get_request("/be/cp/v2/sites/#{site_id}", Site)
+ end
+
+ class Building
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+
+ @[JSON::Field(key: "siteId")]
+ property site_id : String
+
+ property location : Site::Location
+ end
+
+ def buildings(site_id : String)
+ buildings = get_request("/be/cp/v2/sites/#{site_id}/buildings", NamedTuple(buildings: Array(Building)))
+ buildings[:buildings]
+ end
+
+ def building(site_id : String, building_id : String)
+ get_request("/be/cp/v2/sites/#{site_id}/buildings/#{building_id}", Building)
+ end
+
+ class Floor
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+
+ @[JSON::Field(key: "floorNumber")]
+ property floor_number : String
+
+ @[JSON::Field(key: "floorPlanURL")]
+ property floor_plan_url : String
+
+ @[JSON::Field(key: "widthDistance")]
+ property width_distance : Float64
+
+ @[JSON::Field(key: "lengthDistance")]
+ property length_distance : Float64
+
+ # NOTE:: unknown format for referencePoints => Array(?)
+ end
+
+ def floors(site_id : String, building_id : String)
+ floors = get_request("/be/cp/v2/sites/#{site_id}/buildings/#{building_id}/floors", NamedTuple(floors: Array(Building)))
+ floors[:floors]
+ end
+
+ def floor(site_id : String, building_id : String, floor_id : String)
+ get_request("/be/cp/v2/sites/#{site_id}/buildings/#{building_id}/floors/#{floor_id}", Floor)
+ end
+
+ class Area
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+ property length : Float64
+ property width : Float64
+
+ @[JSON::Field(key: "centerX")]
+ property center_x : Float64
+
+ @[JSON::Field(key: "centerY")]
+ property center_y : Float64
+
+ property rotation : Int32
+ property frequency : Int32
+
+ @[JSON::Field(key: "deviceIDs")]
+ property device_ids : Array(String)
+
+ class Application
+ include JSON::Serializable
+
+ @[JSON::Field(key: "areaType")]
+ property area_type : String
+
+ @[JSON::Field(key: "applicationType")]
+ property application_type : String
+ end
+
+ property applications : Array(Application)
+
+ # Area Polygon positions in meters
+ @[JSON::Field(key: "metricPositions")]
+ property metric_positions : Array(MetricPositions)
+
+ # Area Polygon Coordinates positions
+ @[JSON::Field(key: "geoPositions")]
+ property geo_positions : Array(GeoPosition)?
+ end
+
+ class FloorAreas
+ include JSON::Serializable
+
+ @[JSON::Field(key: "floorId")]
+ property floor_id : String
+ property areas : Array(Area)
+ end
+
+ def building_areas(site_id : String, building_id : String)
+ floors = get_request("/be/cp/v2/sites/#{site_id}/buildings/#{building_id}/areas", NamedTuple(
+ floorsAreas: FloorAreas))
+ floors[:floorsAreas]
+ end
+
+ def areas(site_id : String, building_id : String, floor_id : String)
+ areas = get_request("/be/cp/v2/sites/#{site_id}/buildings/#{building_id}/floors/#{floor_id}/areas", NamedTuple(
+ areas: Array(Area)))
+ areas[:areas]
+ end
+
+ def area(site_id : String, building_id : String, floor_id : String, area_id : String)
+ get_request("/be/cp/v2/sites/#{site_id}/buildings/#{building_id}/floors/#{floor_id}/areas/#{area_id}", Area)
+ end
+
+ class Handler
+ include JSON::Serializable
+
+ property id : String
+ property token : String
+
+ @[JSON::Field(key: "thirdPartyAppID")]
+ property app_id : UInt32
+
+ @[JSON::Field(key: "endPoint")]
+ property end_point : String
+ end
+
+ def handlers
+ handlers = get_request("/be/cp/v2/resources/handlers", NamedTuple(
+ handlers: Array(Handler)))
+ handlers[:handlers]
+ end
+
+ class Subscription
+ include JSON::Serializable
+
+ property id : String
+ property token : String
+ property started : Bool
+ property endpoint : String
+ property uri : String
+
+ @[JSON::Field(key: "notificationType")]
+ property notification_type : String
+
+ @[JSON::Field(key: "subscriptionType")]
+ property subscription_type : String
+ end
+
+ enum NotificationType
+ Counting
+ Traffic
+ end
+
+ def subscribe(handler_uri : String, auth_token : String = UUID.random.to_s, events : NotificationType = NotificationType::Counting)
+ # Ensure the handler is a valid URI
+ URI.parse handler_uri
+
+ token = get_token
+ response = post(
+ "/be/cp/v2/telemetry/subscriptions",
+ body: {
+ subscriptionType: "PUSH",
+ notificationType: events.to_s.upcase,
+ endpoint: handler_uri,
+ token: auth_token,
+ }.to_json,
+ headers: {
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ "Authorization" => token,
+ }
+ )
+
+ body = response.body
+ logger.debug { "received login response: #{body}" }
+
+ if response.success?
+ Subscription.from_json(body.not_nil!)
+ else
+ logger.error { "authentication failed with HTTP #{response.status_code}" }
+ raise "failed to obtain access token"
+ end
+ end
+
+ def subscriptions
+ get_request("/be/cp/v2/telemetry/subscriptions", Array(Subscription))
+ end
+
+ def delete_subscription(id : String)
+ token = get_token
+ delete("/be/cp/v2/telemetry/subscriptions/#{id}",
+ headers: {
+ "Accept" => "application/json",
+ "Authorization" => token,
+ }
+ ).success?
+ end
+
+ def update_subscription(id : String, started : Bool = true)
+ token = get_token
+ patch(
+ "/be/cp/v2/telemetry/subscriptions/#{id}",
+ body: {started: started}.to_json,
+ headers: {
+ "Content-Type" => "application/json",
+ "Accept" => "application/json",
+ "Authorization" => token,
+ }
+ ).success?
+ end
+
+ # TODO:: this data is posted to the subscription endpoint
+ # we need to implement webhooks for this to work properly
+ class CountUpdate
+ include JSON::Serializable
+
+ @[JSON::Field(key: "areaId")]
+ property area_id : String
+ property devices : Array(String)
+
+ @[JSON::Field(key: "type")]
+ property event_type : String
+ property timestamp : UInt64
+ property count : Int32
+ end
+
+ def update_count(count_json : String)
+ count = CountUpdate.from_json(count_json)
+ self["area_#{count.area_id}"] = count.count
+ end
+end
diff --git a/drivers/point_grab/cogni_point_spec.cr b/drivers/point_grab/cogni_point_spec.cr
new file mode 100644
index 00000000000..8a305ca31f0
--- /dev/null
+++ b/drivers/point_grab/cogni_point_spec.cr
@@ -0,0 +1,31 @@
+DriverSpecs.mock_driver "PointGrab::CogniPoint" do
+ # Send the request
+ retval = exec(:get_token)
+ token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJSRUFEIiwiV1JJVEUiXSwiZXhwIjoxNTc0MjMzNjEyLCJhdXRob3JpdGllcyI6WyJST0xFX1RSVVNURURfQ0xJRU5UIl0sImp0aSI6IjM1ZjkxYjlkLTVmZmMtNDJkYy05YWZkLTJiZTE0YjI1MmE1NCIsImNsaWVudF9pZCI6IjEwMDAwMjEzIn0.Wzrsaey5z3ShAFYKOaWmgfoRZNsk-PclSK9IRtYf4b8"
+
+ # We should request a new token from Floorsense
+ expect_http_request do |request, response|
+ if io = request.body
+ data = io.gets_to_end
+
+ # The request is param encoded
+ if data == "grant_type=client_credentials" && request.headers["Authorization"] == "Basic #{Base64.strict_encode("10000000:c5a6adc6-UUID-46e8-b72d-91395bce9565")}"
+ response.status_code = 200
+ response.output.puts %({
+ "token": "#{token}",
+ "access_token": "#{token}",
+ "token_type": "bearer",
+ "expires_in": 3599
+ })
+ else
+ response.status_code = 401
+ response.output.puts ""
+ end
+ else
+ raise "expected request to include token type"
+ end
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq("Bearer #{token}")
+end
diff --git a/drivers/qsc/q_sys_control.cr b/drivers/qsc/q_sys_control.cr
new file mode 100644
index 00000000000..ed60cf0cdf2
--- /dev/null
+++ b/drivers/qsc/q_sys_control.cr
@@ -0,0 +1,377 @@
+# Documentation https://q-syshelp.qsc.com/Content/External_Control/Q-SYS_External_Control/007_Q-SYS_External_Control_Protocol.htm
+
+class Qsc::QSysControl < PlaceOS::Driver
+ # Discovery Information
+ tcp_port 1702
+ descriptive_name "QSC Audio DSP External Control"
+ generic_name :Mixer
+
+ alias Group = NamedTuple(id: Int32, controls: Set(String))
+ alias Ids = String | Array(String)
+ alias Val = Int32 | Float64
+
+ @username : String? = nil
+ @password : String? = nil
+ @change_group_id : Int32 = 30
+ @em_id : String? = nil
+ @emergency_subscribe : PlaceOS::Driver::Subscriptions::Subscription? = nil
+ @history = {} of String => Symbol
+ @change_groups = {} of Symbol => Group
+
+ def on_load
+ transport.tokenizer = Tokenizer.new("\r\n")
+ on_update
+ end
+
+ def on_update
+ @username = setting?(String, :username)
+ @password = setting?(String, :password)
+ login if @username
+
+ @change_groups.each do |_, group|
+ logger.debug { "change groups" }
+ group_id = group[:id]
+ controls = group[:controls]
+
+ # Re-create change groups and poll every 2 seconds
+ do_send("cgc #{group_id}\n") # , wait: false)
+ do_send("cgsna #{group_id} 2000\n") # , wait: false)
+ controls.each do |id|
+ do_send("cga #{group_id} #{id}\n") # , wait: false)
+ end
+ end
+
+ em_id = setting?(String, :emergency)
+
+ # Emergency ID changed
+ if (e = @emergency_subscribe) && @em_id != em_id
+ subscriptions.unsubscribe(e)
+ end
+
+ # Emergency ID exists
+ if em_id
+ group = create_change_group(:emergency)
+ group_id = group[:id]
+ controls = group[:controls]
+
+ # Add id to change group as required
+ unless controls.includes?(em_id)
+ # subscribe to changes
+ @em_id = em_id
+ @emergency_subscribe = subscribe(em_id) do |_, value|
+ self[:emergency] = value
+ end
+
+ update_change_group(:emergency, group_id, Set.new([em_id]))
+ do_send("cga #{group_id} #{em_id}\n") # , wait: false)
+ end
+ end
+ end
+
+ def connected
+ schedule.every(40.seconds) do
+ logger.debug { "Maintaining Connection" }
+ about
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def get_status(control_id : String, **options)
+ do_send("cg #{control_id}\n", **options)
+ end
+
+ def set_position(control_id : String, position : Int32, ramp_time : Val? = nil)
+ if ramp_time
+ do_send("cspr \"#{control_id}\" #{position} #{ramp_time}\n") # , wait: false)
+ schedule.in(ramp_time.seconds + 200.milliseconds) { get_status(control_id) }
+ else
+ do_send("csp \"#{control_id}\" #{position}\n")
+ end
+ end
+
+ def set_value(control_id : String, value : Val, ramp_time : Val? = nil, **options)
+ if ramp_time
+ do_send("csvr \"#{control_id}\" #{value} #{ramp_time}\n", **options) # , wait: false)
+ schedule.in(ramp_time.seconds + 200.milliseconds) { get_status(control_id) }
+ else
+ do_send("csv \"#{control_id}\" #{value}\n", **options)
+ end
+ end
+
+ def about
+ do_send("sg\n", name: :status, priority: 0)
+ end
+
+ def login(username : String? = nil, password : String? = nil)
+ username ||= @username
+ password ||= @password
+ do_send("login #{username} #{password}\n", name: :login, priority: 99)
+ end
+
+ # Used to set a dial number/string
+ def set_string(control_ids : Ids, text : String)
+ ensure_array(control_ids).each do |id|
+ do_send("css \"#{id}\" \"#{text}\"\n").get
+ self[id] = text
+ end
+ end
+
+ # Used to trigger dialing etc
+ def trigger(control_id : String)
+ logger.debug { "Sending trigger to Qsys: ct #{control_id}" }
+ do_send("ct \"#{control_id}\"\n") # , wait: false)
+ end
+
+ # Compatibility Methods
+ def fader(fader_ids : Ids, level : Int32)
+ level = level / 10
+ ensure_array(fader_ids).each { |f_id| set_value(f_id, level, name: "fader#{f_id}", fader_type: :fader) }
+ end
+
+ def faders(fader_ids : Ids, level : Int32)
+ fader(fader_ids, level)
+ end
+
+ def mute(mute_ids : Ids, state : Bool = true)
+ level = state ? 1 : 0
+ ensure_array(mute_ids).each { |m_id| set_value(m_id, level, fader_type: :mute) }
+ end
+
+ def mutes(mute_ids : Ids, state : Bool)
+ mute(mute_ids, state)
+ end
+
+ def unmute(mute_ids : Ids)
+ mute(mute_ids, false)
+ end
+
+ def mute_toggle(mute_id : Ids)
+ mute(mute_id, !self["fader#{mute_id}_mute"].try(&.as_bool))
+ end
+
+ def snapshot(name : String, index : Int32, ramp_time : Val = 1.5)
+ do_send("ssl \"#{name}\" #{index} #{ramp_time}\n") # , wait: false)
+ end
+
+ def save_snapshot(name : String, index : Int32)
+ do_send("sss \"#{name}\" #{index}\n") # , wait: false)
+ end
+
+ # For inter-module compatibility
+ def query_fader(fader_ids : Ids)
+ fad = ensure_array(fader_ids)[0]
+ get_status(fad, fader_type: :fader)
+ end
+
+ def query_faders(fader_ids : Ids)
+ ensure_array(fader_ids).each { |f_id| get_status(f_id, fader_type: :fader) }
+ end
+
+ def query_mute(fader_ids : Ids)
+ fad = ensure_array(fader_ids)[0]
+ get_status(fad, fader_type: :mute)
+ end
+
+ def query_mutes(fader_ids : Ids)
+ ensure_array(fader_ids).each { |fad| get_status(fad, fader_type: :mute) }
+ end
+
+ def phone_number(number : String, control_id : String)
+ set_string(control_id, number)
+ end
+
+ def phone_dial(control_id : String)
+ trigger(control_id)
+ schedule.in(200.milliseconds) { poll_change_group(:phone) }
+ end
+
+ def phone_hangup(control_id : String)
+ phone_dial(control_id)
+ end
+
+ def phone_watch(control_ids : Ids)
+ # Ensure change group exists
+ group = create_change_group(:phone)
+ group_id = group[:id]
+ controls = group[:controls]
+
+ # Add ids to change group
+ ensure_array(control_ids).each do |id|
+ unless controls.includes?(id)
+ controls << id
+ do_send("cga #{group_id} #{id}\n") # , wait: false)
+ end
+ end
+
+ update_change_group(:phone, group_id, controls)
+ end
+
+ private def create_change_group(name) : Group
+ if group = @change_groups[name]?
+ return group
+ end
+
+ # Provide a unique group id
+ next_id = @change_group_id
+ @change_group_id += 1
+
+ @change_groups[name] = {
+ id: next_id,
+ controls: Set(String).new,
+ }
+
+ # create change group and poll every 2 seconds
+ do_send("cgc #{next_id}\n") # , wait: false)
+ do_send("cgsna #{next_id} 2000\n") # , wait: false)
+ @change_groups[name]
+ end
+
+ private def update_change_group(name, id, controls) : Group
+ @change_groups[name] = {
+ id: id,
+ controls: controls,
+ }
+ end
+
+ private def poll_change_group(name)
+ if group = @change_groups[name]
+ do_send("cgpna #{group[:id]}\n") # , wait: false)
+ end
+ end
+
+ def received(data, task)
+ process_response(data, task)
+ end
+
+ private def process_response(data, task, fader_type : Symbol? = nil)
+ data = String.new(data)
+ return task.try(&.success) if data == "none\r\n"
+ logger.debug { "QSys sent: #{data}" }
+ resp = shellsplit(data)
+
+ case resp[0]
+ when "cv"
+ control_id = resp[1]
+ # string rep = resp[2]
+ value = resp[3]
+ position = resp[4].to_i
+
+ self["pos_#{control_id}"] = position
+
+ if type = fader_type || @history[control_id]?
+ @history[control_id] = type
+
+ case type
+ when :fader
+ self["fader#{control_id}"] = (value.to_f * 10).to_i
+ when :mute
+ self["fader#{control_id}_mute"] = value.to_i == 1
+ end
+ else
+ value = resp[2]
+ if value == "false" || value == "true"
+ self[control_id] = value == "true"
+ else
+ self[control_id] = value.gsub('_', ' ')
+ end
+ logger.debug { "Received response from unknown ID type: #{control_id} == #{value}" }
+ end
+ when "cvv" # Control status, Array of control status
+ control_id = resp[1]
+ count = resp[2].to_i
+
+ if type = fader_type || @history[control_id]?
+ @history[control_id] = type
+
+ # Skip strings and extract the values
+ next_count = count + 3
+ count = resp[next_count].to_i
+ 1.upto(count) do |index|
+ value = resp[next_count + index]
+
+ case type
+ when :fader
+ self["fader#{control_id}"] = (value.to_f * 10).to_i
+ when :mute
+ self["fader#{control_id}_mute"] = value == 1
+ end
+ end
+ else
+ # Don't skip strings here
+ next_count = 2
+ 1.upto(count) do |index|
+ value = resp[next_count + index]
+
+ if value == "false" || value == "true"
+ self[control_id] = value == "true"
+ else
+ self[control_id] = value.gsub('_', ' ')
+ end
+ end
+ logger.debug { "Received response from unknown ID type: #{control_id}" }
+
+ # Jump to the position values
+ next_count = count + 3
+ count = resp[next_count].to_i
+ end
+
+ # Grab the positions
+ next_count = next_count + count + 1
+ count = resp[next_count].to_i
+ 1.upto(count) do |index|
+ value = resp[next_count + index]
+ self["pos_#{control_id}"] = value
+ end
+ when "sr" # About response
+ self[:design_name] = resp[1]
+ self[:is_primary] = resp[3] == "1"
+ self[:is_active] = resp[4] == "1"
+ when "core_not_active", "bad_change_group_handle", "bad_command", "bad_id", "control_read_only", "too_many_change_groups"
+ return task.try(&.abort("Error response received: #{data}"))
+ when "login_required"
+ login if @username
+ return task.try(&.abort("Login is required!"))
+ when "login_success"
+ logger.debug { "Login success!" }
+ when "login_failed"
+ return task.try(&.abort("Invalid login details provided"))
+ when "rc"
+ logger.warn { "System is notifying us of a disconnect!" }
+ when "cmvv"
+ logger.debug { "received cmvv response" }
+ else
+ logger.warn { "Unknown response received #{data}" }
+ end
+
+ task.try(&.success)
+ end
+
+ private def do_send(req, fader_type : Symbol? = nil, **options)
+ logger.debug { "sending #{req}" }
+ send(req, **options) { |data, task| process_response(data, task, fader_type) }
+ end
+
+ private def ensure_array(object)
+ object.is_a?(Array) ? object : [object]
+ end
+
+ # Quick dirty port of https://github.com/ruby/ruby/blob/master/lib/shellwords.rb
+ private def shellsplit(line : String) : Array(String)
+ words = [] of String
+ field = ""
+ pattern = /\G\s*(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m
+ line.scan(pattern) do |match|
+ _, word, sq, dq, esc, garbage, sep = match.to_a
+ raise ArgumentError.new("Unmatched quote: #{line.inspect}") if garbage
+ field += (word || sq || dq.try(&.gsub(/\\([$`"\\\n])/, "\\1")) || esc.not_nil!.gsub(/\\(.)/, "\\1"))
+ if sep
+ words << field
+ field = ""
+ end
+ end
+ words
+ end
+end
diff --git a/drivers/qsc/q_sys_control_spec.cr b/drivers/qsc/q_sys_control_spec.cr
new file mode 100644
index 00000000000..6a49d612b39
--- /dev/null
+++ b/drivers/qsc/q_sys_control_spec.cr
@@ -0,0 +1,70 @@
+DriverSpecs.mock_driver "Qsc::QSysControl" do
+ settings({
+ username: "user",
+ password: "pass",
+ emergency: "6",
+ })
+
+ should_send("login user pass\n")
+ responds("login_success\r\n")
+ should_send("cgc 30\n")
+ responds("none\r\n")
+ should_send("cgsna 30 2000\n")
+ responds("none\r\n")
+ should_send("cga 30 6\n")
+ responds("none\r\n")
+
+ exec(:about)
+ should_send("sg\n")
+ responds("sr \"MyDesign\" \"NIEC2bxnVZ6a\" 1 1\r\n")
+ status[:design_name].should eq("MyDesign")
+ status[:is_primary].should eq(true)
+ status[:is_active].should eq(true)
+
+ exec(:mute, ["1", "2", "3"], true)
+ should_send("csv \"1\" 1\n")
+ responds("cv \"1\" \"control string\" 1 8\r\n")
+ status[:pos_1].should eq(8)
+ status[:fader1_mute].should eq(true)
+ should_send("csv \"2\" 1\n")
+ responds("cv \"2\" \"control string\" 1 5\r\n")
+ status[:pos_2].should eq(5)
+ status[:fader2_mute].should eq(true)
+ should_send("csv \"3\" 1\n")
+ responds("cv \"3\" \"control string\" 1 4\r\n")
+ status[:pos_3].should eq(4)
+ status[:fader3_mute].should eq(true)
+
+ exec(:faders, ["1", "2", "3"], 90)
+ should_send("csv \"1\" 9.0\n")
+ responds("cv \"1\" \"control string\" 9 6\r\n")
+ status[:pos_1].should eq(6)
+ status[:fader1_mute].should eq(true)
+ should_send("csv \"2\" 9.0\n")
+ responds("cv \"2\" \"control string\" 9 7\r\n")
+ status[:pos_2].should eq(7)
+ status[:fader2].should eq(90)
+ should_send("csv \"3\" 9.0\n")
+ responds("cv \"3\" \"control string\" 9 8\r\n")
+ status[:pos_3].should eq(8)
+ status[:fader3].should eq(90)
+
+ exec(:phone_watch, "0")
+ should_send("cgc 31\n")
+ responds("none\r\n")
+ should_send("cgsna 31 2000\n")
+ responds("none\r\n")
+ should_send("cga 31 0\n")
+ responds("none\r\n")
+
+ exec(:phone_watch, ["1", "2"])
+ should_send("cga 31 1\n")
+ responds("none\r\n")
+ should_send("cga 31 2\n")
+ responds("none\r\n")
+
+ exec(:phone_number, "0123456789", "1")
+ should_send("css \"1\" \"0123456789\"\n")
+ responds("cv \"1\" \"0123456789\" 9 8\r\n")
+ status[:"1"].should eq("0123456789")
+end
diff --git a/drivers/qsc/q_sys_remote.cr b/drivers/qsc/q_sys_remote.cr
new file mode 100644
index 00000000000..27cc49ff1b1
--- /dev/null
+++ b/drivers/qsc/q_sys_remote.cr
@@ -0,0 +1,416 @@
+require "json"
+
+# Documentation: https://aca.im/driver_docs/QSC/QRCDocumentation.pdf
+
+class Qsc::QSysRemote < PlaceOS::Driver
+ tcp_port 1710
+ descriptive_name "QSC Audio DSP"
+ generic_name :Mixer
+
+ @id : Int32 = 0
+ @db_based_faders : Bool? = nil
+ @integer_faders : Bool? = nil
+ @username : String? = nil
+ @password : String? = nil
+
+ Delimiter = "\0"
+ JsonRpcVer = "2.0"
+ Errors = {
+ -32700 => "Parse error. Invalid JSON was received by the server.",
+ -32600 => "Invalid request. The JSON sent is not a valid Request object.",
+ -32601 => "Method not found.",
+ -32602 => "Invalid params.",
+ -32603 => "Server error.",
+ 2 => "Invalid Page Request ID",
+ 3 => "Bad Page Request - could not create the requested Page Request",
+ 4 => "Missing file",
+ 5 => "Change Groups exhausted",
+ 6 => "Unknown change croup",
+ 7 => "Unknown component name",
+ 8 => "Unknown control",
+ 9 => "Illegal mixer channel index",
+ 10 => "Logon required",
+ }
+
+ alias Num = Int32 | Float64
+ alias ValTup = NamedTuple(Name: String, Value: Num)
+ alias PosTup = NamedTuple(Name: String, Position: Num)
+ alias Values = ValTup | PosTup | Array(ValTup) | Array(PosTup)
+ alias Ids = String | Array(String)
+
+ def on_load
+ transport.tokenizer = Tokenizer.new(Delimiter)
+ on_update
+ end
+
+ def on_update
+ @db_based_faders = setting?(Bool, :db_based_faders)
+ @integer_faders = setting?(Bool, :integer_faders)
+ @username = setting?(String, :username)
+ @password = setting?(String, :password)
+ logon if @username && @password
+ end
+
+ def connected
+ schedule.every(20.seconds) do
+ logger.debug { "Maintaining Connection" }
+ no_op
+ end
+ @id = 0
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ # This command does nothing but is useful for making sure the socket is left open
+ def no_op
+ do_send(cmd: :NoOp, priority: 0)
+ end
+
+ def get_status
+ do_send(next_id, cmd: :StatusGet, params: 0, priority: 0)
+ end
+
+ def logon
+ do_send(
+ cmd: :Logon,
+ params: {
+ :User => @username,
+ :Password => @password,
+ },
+ priority: 99
+ )
+ end
+
+ def control_set(name : String, value : Num | Bool, ramp : Num? = nil, **options)
+ if ramp
+ params = {
+ :Name => name,
+ :Value => value,
+ :Ramp => ramp,
+ }
+ else
+ params = {
+ :Name => name,
+ :Value => value,
+ }
+ end
+
+ do_send(next_id, "Control.Set", params, **options)
+ end
+
+ def control_get(names : Array(String), **options)
+ do_send(next_id, "Control.Get", names, **options)
+ end
+
+ def component_get(c_name : String, controls : Array(String), **options)
+ do_send(next_id, "Component.Get", {
+ :Name => c_name,
+ :Controls => controls.map { |ctrl| {:Name => ctrl} },
+ }, **options)
+ end
+
+ def component_set(c_name : String, values : Values, **options)
+ values = ensure_array(values)
+
+ do_send(next_id, "Component.Set", {
+ :Name => c_name,
+ :Controls => values,
+ }, **options)
+ end
+
+ def component_trigger(component : String, trigger : String, **options)
+ do_send(next_id, "Component.Trigger", {
+ :Name => component,
+ :Controls => [{:Name => trigger}],
+ }, **options)
+ end
+
+ def get_components(**options)
+ do_send(next_id, "Component.GetComponents", **options)
+ end
+
+ def change_group_add_controls(group_id : String, controls : Array(String), **options)
+ do_send(next_id, "ChangeGroup.AddControl", {
+ :Id => group_id,
+ :Controls => controls,
+ }, **options)
+ end
+
+ def change_group_remove_controls(group_id : String, controls : Array(String), **options)
+ do_send(next_id, "ChangeGroup.Remove", {
+ :Id => group_id,
+ :Controls => controls,
+ }, **options)
+ end
+
+ def change_group_add_component(group_id : String, component_name : String, controls : Array(String), **options)
+ do_send(next_id, "ChangeGroup.AddComponentControl", {
+ :Id => group_id,
+ :Component => {
+ :Name => component_name,
+ :Controls => controls.map { |ctrl| {:Name => ctrl} },
+ },
+ }, **options)
+ end
+
+ # Returns values for all the controls
+ def poll_change_group(group_id : String, **options)
+ do_send(next_id, "ChangeGroup.Poll", {:Id => group_id}, **options)
+ end
+
+ # Removes the change group
+ def destroy_change_group(group_id : String, **options)
+ do_send(next_id, "ChangeGroup.Destroy", {:Id => group_id}, **options)
+ end
+
+ # Removes all controls from change group
+ def clear_change_group(group_id : String, **options)
+ do_send(next_id, "ChangeGroup.Clear", {:Id => group_id}, **options)
+ end
+
+ # Where every is the number of seconds between polls
+ def auto_poll_change_group(group_id : String, every : Num, **options)
+ do_send(next_id, "ChangeGroup.AutoPoll", {
+ :Id => group_id,
+ :Rate => every,
+ }, **options) # , wait: false)
+ end
+
+ # Example usage:
+ # mixer 'Parade', {1 => [2,3,4], 3 => 6}, true
+ def mixer(name : String, inouts : Hash(Int32, Int32 | Array(Int32)), mute : Bool = false, **options)
+ inouts.each do |input, outputs|
+ outputs = ensure_array(outputs)
+
+ do_send(next_id, "Mixer.SetCrossPointMute", {
+ :Name => name,
+ :Inputs => input.to_s,
+ :Outputs => outputs.join(' '),
+ :Value => mute,
+ }, **options)
+ end
+ end
+
+ Faders = {
+ matrix_in: {
+ type: :"Mixer.SetInputGain",
+ pri: :Inputs,
+ },
+ matrix_out: {
+ type: :"Mixer.SetOutputGain",
+ pri: :Outputs,
+ },
+ matrix_crosspoint: {
+ type: :"Mixer.SetCrossPointGain",
+ pri: :Inputs,
+ sec: :Outputs,
+ },
+ }
+
+ def matrix_fader(name : String, level : Num, index : Array(Int32), type : String = "matrix_out", **options)
+ info = Faders[type]
+
+ if sec = info[:sec]?
+ params = {
+ :Name => name,
+ info[:pri] => index[0],
+ sec => index[1],
+ :Value => level,
+ }
+ else
+ params = {
+ :Name => name,
+ info[:pri] => index,
+ :Value => level,
+ }
+ end
+
+ do_send(next_id, info[:type], params, **options)
+ end
+
+ Mutes = {
+ matrix_in: {
+ type: :"Mixer.SetInputMute",
+ pri: :Inputs,
+ },
+ matrix_out: {
+ type: :"Mixer.SetOutputMute",
+ pri: :Outputs,
+ },
+ }
+
+ def matrix_mute(name : String, value : Num, index : Array(Int32), type : String = "matrix_out", **options)
+ info = Mutes[type]
+
+ do_send(next_id, info[:type], {
+ :Name => name,
+ info[:pri] => index,
+ :Value => value,
+ }, **options)
+ end
+
+ # value can either be a number to set actual numeric values like decibels
+ # or Bool to deal with mute state
+ def fader(fader_ids : Ids, value : Num | Bool, component : String? = nil, type : String = "fader", use_value : Bool = false, **options)
+ faders = ensure_array(fader_ids)
+ if component && (val = value.as?(Num))
+ if @db_based_faders || use_value
+ val = val / 10 if @integer_faders && !use_value
+ fads = faders.map { |fad| {Name: fad, Value: val} }
+ else
+ val = val / 1000 if @integer_faders
+ fads = faders.map { |fad| {Name: fad, Position: val} }
+ end
+ component_set(component, fads, name: "level_#{faders[0]}").get
+ component_get(component, faders)
+ else
+ reqs = faders.map { |fad| control_set(fad, value) }
+ reqs.last.get
+ control_get(faders)
+ end
+ end
+
+ def faders(ids : Ids, value : Num | Bool, component : String? = nil, type : String = "fader", **options)
+ fader(ids, value, component, type, **options)
+ end
+
+ def mute(fader_id : Ids, state : Bool = true, component : String? = nil, type : String = "fader", **options)
+ fader(fader_id, state, component, type, state, **options)
+ end
+
+ def mutes(ids : Ids, state : Bool = true, component : String? = nil, type : String = "fader", **options)
+ mute(ids, state, component, type, **options)
+ end
+
+ def unmute(fader_id : Ids, component : String? = nil, type : String = "fader", **options)
+ mute(fader_id, false, component, type, **options)
+ end
+
+ def query_fader(fader_id : Ids, component : String? = nil, type : String = "fader")
+ faders = ensure_array(fader_id)
+ component ? component_get(component, faders) : control_get(faders)
+ end
+
+ def query_faders(ids : Ids, component : String? = nil, type : String = "fader", **options)
+ query_fader(ids, component, type, **options)
+ end
+
+ def query_mute(fader_id : Ids, component : String? = nil, type : String = "fader")
+ query_fader(fader_id, component, type)
+ end
+
+ def query_mutes(ids : Ids, component : String? = nil, type : String = "fader", **options)
+ query_fader(ids, component, type, **options)
+ end
+
+ def received(data, task)
+ data = String.new(data[0..-2])
+ response = JSON.parse(data)
+
+ logger.debug { "QSys sent:" }
+ logger.debug { response }
+
+ if err = response["error"]?
+ code = err["code"]
+ logger.warn { "Error code #{code} - #{Errors[code]}" }
+
+ if code == 10
+ if @username && @password
+ logon.get
+ return task.try(&.retry("Logged on and retrying command"))
+ else
+ return task.try(&.abort("Login required but no username and/or password in settings"))
+ end
+ end
+
+ return task.try(&.abort(err["message"]))
+ end
+
+ return task.try(&.success("Unknown response")) unless result = response["result"]?
+
+ case result
+ when .as_h?
+ if result["Controls"]? # Probably Component.Get
+ process(result["Controls"].as_a, result["Name"]?)
+ elsif result["Platform"]? # StatusGet
+ result.as_h.each { |k, v| self[k.underscore] = v }
+ end
+ when .as_a? # Control.Get
+ process(result.as_a)
+ end
+
+ task.try(&.success)
+ end
+
+ BoolVals = ["true", "false"]
+
+ private def process(values : Array, name : JSON::Any? = nil)
+ component = name.try(&.as_s?) ? "_#{name}" : ""
+ values.each do |value|
+ name = value["Name"]
+
+ next unless val = value["Value"]?
+
+ pos = value["Position"]?
+ str = value["String"]?.try(&.as_s)
+
+ if BoolVals.includes?(str)
+ self["fader#{name}#{component}_mute"] = str == "true"
+ else
+ # Seems like string values can be independent of the other values
+ # This should mostly work to detect a string value
+ if val == 0 && pos == 0 && str && str[0] != '0'
+ self["#{name}#{component}"] = str
+ next
+ end
+
+ if pos && (pos = pos.as_i? || pos.as_f?)
+ self["fader#{name}#{component}_pos"] = @integer_faders ? (pos * 1000).to_i : pos
+ end
+
+ if val.as_s?
+ self["#{name}#{component}"] = val
+ elsif val = (val.as_i? || val.as_f?)
+ self["fader#{name}#{component}_val"] = @integer_faders ? (val * 10).to_i : val
+ end
+ end
+ end
+ end
+
+ def next_id
+ @id += 1
+ @id
+ end
+
+ private def do_send(id : Int32? = nil, cmd = nil, params = {} of String => String, **options)
+ if id
+ req = {
+ jsonrpc: JsonRpcVer,
+ id: id,
+ method: cmd,
+ params: params,
+ }
+ else
+ req = {
+ jsonrpc: JsonRpcVer,
+ method: cmd,
+ params: params,
+ }
+ end
+
+ logger.debug { "sending: #{req}" }
+
+ cmd = req.to_json + Delimiter
+
+ logger.debug { "sending json" }
+ logger.debug { cmd.inspect }
+
+ send(cmd, **options)
+ end
+
+ private def ensure_array(object)
+ object.is_a?(Array) ? object : [object]
+ end
+end
diff --git a/drivers/qsc/q_sys_remote_spec.cr b/drivers/qsc/q_sys_remote_spec.cr
new file mode 100644
index 00000000000..442e588ae21
--- /dev/null
+++ b/drivers/qsc/q_sys_remote_spec.cr
@@ -0,0 +1,149 @@
+DriverSpecs.mock_driver "Qsc::QSysRemote" do
+ settings({
+ username: "user",
+ password: "pass",
+ })
+
+ # logon
+ should_send({
+ jsonrpc: "2.0",
+ method: "Logon",
+ params: {
+ "User" => "user",
+ "Password" => "pass",
+ },
+ }.to_json + "\0")
+ responds({"TODO" => "response not defined in docs"}.to_json + "\0")
+
+ exec(:no_op)
+ should_send({
+ jsonrpc: "2.0",
+ method: "NoOp",
+ params: {} of String => String,
+ }.to_json + "\0")
+ responds({"TODO" => "response not defined in docs"}.to_json + "\0")
+
+ exec(:get_status)
+ should_send({
+ jsonrpc: "2.0",
+ id: 1,
+ method: "StatusGet",
+ params: 0,
+ }.to_json + "\0")
+ responds({
+ "jsonrpc" => "2.0",
+ "id" => 1,
+ "result" => {
+ "Platform" => "Core 500i",
+ "State" => "Active",
+ "DesignName" => "SAF‐MainPA",
+ "DesignCode" => "qALFilm6IcAz",
+ "IsRedundant" => false,
+ "IsEmulator" => true,
+ "Status" => {
+ "Code" => 0,
+ "String" => "OK",
+ },
+ },
+ }.to_json + "\0")
+ status[:platform].should eq("Core 500i")
+ status[:state].should eq("Active")
+ status[:design_name].should eq("SAF‐MainPA")
+ status[:design_code].should eq("qALFilm6IcAz")
+ status[:is_redundant].should eq(false)
+ status[:is_emulator].should eq(true)
+ status[:status].should eq({
+ "Code" => 0,
+ "String" => "OK",
+ })
+
+ exec(:control_set, "MainGain", -12)
+ should_send({
+ "jsonrpc" => "2.0",
+ "id" => 2,
+ "method" => "Control.Set",
+ "params" => {
+ "Name" => "MainGain",
+ "Value" => -12,
+ },
+ }.to_json + "\0")
+ responds({
+ "jsonrpc" => "2.0",
+ "id" => 1234,
+ "result" => [
+ {
+ "Name" => "MainGain",
+ "Value" => -12,
+ },
+ ],
+ }.to_json + "\0")
+ status[:faderMainGain_val].should eq(-12)
+
+ exec(:component_get, "My APM", ["ent.xfade.gain", "ent.xfade.gain2"])
+ should_send({
+ "jsonrpc" => "2.0",
+ "id" => 3,
+ "method" => "Component.Get",
+ "params" => {
+ "Name" => "My APM",
+ "Controls" => [
+ {"Name" => "ent.xfade.gain"},
+ {"Name" => "ent.xfade.gain2"},
+ ],
+ },
+ }.to_json + "\0")
+ responds({
+ "jsonrpc" => "2.0",
+ "result" => {
+ "Name" => "My APM",
+ "Controls" => [
+ {
+ "Name" => "ent.xfade.gain",
+ "Value" => -100.0,
+ "String" => "‐100.0dB",
+ "Position" => 0,
+ },
+ {
+ "Name" => "ent.xfade.gain2",
+ "Value" => -50.0,
+ "String" => "‐50.0dB",
+ "Position" => 0,
+ },
+ ],
+ },
+ }.to_json + "\0")
+ status["faderent.xfade.gain_My APM_pos"].should eq(0)
+ status["faderent.xfade.gain_My APM_val"].should eq(-100)
+ status["faderent.xfade.gain2_My APM_pos"].should eq(0)
+ status["faderent.xfade.gain2_My APM_val"].should eq(-50)
+
+ exec(:change_group_add_controls, "my change group", ["some control", "another control"])
+ should_send({
+ "jsonrpc" => "2.0",
+ "id" => 4,
+ "method" => "ChangeGroup.AddControl",
+ "params" => {
+ "Id" => "my change group",
+ "Controls" => ["some control", "another control"],
+ },
+ }.to_json + "\0")
+ responds({
+ "jsonrpc" => "2.0",
+ "id" => 4,
+ "result" => {
+ "Id" => "my change group",
+ "Changes" => [
+ {
+ "Name" => "some control",
+ "Value" => -12,
+ "String" => "‐12dB",
+ },
+ {
+ "Name" => "another control",
+ "Value" => -6,
+ "String" => "‐6dB",
+ },
+ ],
+ },
+ }.to_json + "\0")
+end
diff --git a/drivers/samsung/displays/mdc_protocol.cr b/drivers/samsung/displays/mdc_protocol.cr
new file mode 100644
index 00000000000..e071ff71a21
--- /dev/null
+++ b/drivers/samsung/displays/mdc_protocol.cr
@@ -0,0 +1,346 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+class Samsung::Displays::MDCProtocol < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ INDICATOR = 0xAA_u8
+
+ enum Input
+ Vga = 0x14 # pc in manual
+ Dvi = 0x18
+ DviVideo = 0x1F
+ Hdmi = 0x21
+ HdmiPc = 0x22
+ Hdmi2 = 0x23
+ Hdmi2Pc = 0x24
+ Hdmi3 = 0x31
+ Hdmi3Pc = 0x32
+ Hdmi4 = 0x33
+ Hdmi4Pc = 0x34
+ DisplayPort = 0x25
+ Dtv = 0x40
+ Media = 0x60
+ Widi = 0x61
+ MagicInfo = 0x20
+ Whiteboard = 0x64
+ end
+
+ include Interface::InputSelection(Input)
+
+ # Discovery Information
+ tcp_port 1515
+ descriptive_name "Samsung MD, DM & QM Series LCD"
+ generic_name :Display
+
+ # Markdown description
+ description <<-DESC
+ For DM displays configure the following 1:
+
+ 1. Network Standby = ON
+ 2. Set Auto Standby = OFF
+ 3. Set Eco Solution, Auto Off = OFF
+
+ Hard Power off displays each night and hard power ON in the morning.
+ DESC
+
+ default_settings({
+ display_id: 0,
+ rs232_control: false,
+ })
+
+ @id : UInt8 = 0
+ @rs232 : Bool = false
+ @blank : Input?
+ @previous_volume : Int32 = 50
+ @input_target : Input? = nil
+ @power_target : Bool? = nil
+
+ def on_load
+ transport.tokenizer = Tokenizer.new do |io|
+ bytes = io.peek
+ # Ensure message indicator is well-formed
+ disconnect unless bytes.first == INDICATOR
+ logger.debug { "Received: #{bytes}" }
+ # [header, command, id, data.size, [data], checksum]
+ # return 0 if the message is incomplete
+ bytes.size < 4 ? 0 : bytes[3].to_i + 5
+ end
+
+ on_update
+ end
+
+ def on_update
+ @id = setting(UInt8, :display_id)
+ @rs232 = setting(Bool, :rs232_control)
+ @blank = setting?(String, :blanking_input).try &->Input.parse(String)
+ end
+
+ def connected
+ do_device_config unless self[:hard_off]?.try &.as_bool
+
+ schedule.every(30.seconds, true) do
+ do_poll
+ end
+ end
+
+ def disconnected
+ self[:power] = false unless @rs232
+ schedule.clear
+ end
+
+ # As true power off disconnects the server we only want to power off the panel
+ def power(state : Bool)
+ @power_target = state
+
+ if state
+ # Power on
+ do_send(Command::HardOff, 1)
+ do_send(Command::PanelMute, 0)
+ else
+ # Blank the screen before turning off panel if required
+ # required by some video walls where screens are chained
+ if (blanking_input = @blank) && self[:power]?
+ switch_to(blanking_input)
+ end
+ do_send(Command::PanelMute, 1)
+ end
+ end
+
+ def hard_off
+ do_send(Command::PanelMute, 0) if self[:power]?.try &.as_bool
+ do_send(Command::HardOff, 0)
+ end
+
+ def power?(**options) : Bool
+ do_send(Command::PanelMute, Bytes.empty, **options).get
+ !!self[:power]?.try(&.as_bool)
+ end
+
+ # Mutes both audio/video
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ mute_video(state) if layer.video? || layer.audio_video?
+ mute_audio(state) if layer.audio? || layer.audio_video?
+ end
+
+ # Adds video mute state compatible with projectors
+ def mute_video(state : Bool = true)
+ state = state ? 1 : 0
+ do_send(Command::PanelMute, state)
+ end
+
+ # Emulate audio mute
+ def mute_audio(state : Bool = true)
+ # Do nothing if already in desired state
+ return if self[:audio_mute]?.try(&.as_bool) == state
+ self[:audio_mute] = state
+ if state
+ @previous_volume = self[:volume]?.try(&.as_i) || 0
+ volume(0)
+ else
+ volume(@previous_volume)
+ end
+ end
+
+ # check software version
+ def software_version
+ do_send(Command::SoftwareVersion)
+ end
+
+ def serial_number
+ do_send(Command::SerialNumber)
+ end
+
+ def switch_to(input : Input, **options)
+ @input_target = input
+ do_send(Command::Input, input.value, **options)
+ end
+
+ enum SpeakerMode
+ Internal = 0
+ External = 1
+ end
+
+ def speaker_select(mode : SpeakerMode, **options)
+ do_send(Command::Speaker, mode.value, **options)
+ end
+
+ def do_poll
+ do_send(Command::Status, Bytes.empty, priority: 0)
+ power? unless self[:hard_off]?.try &.as_bool
+ end
+
+ DEVICE_SETTINGS = {
+ network_standby: Bool,
+ auto_off_timer: Bool,
+ auto_power: Bool,
+ volume: Int32,
+ contrast: Int32,
+ brightness: Int32,
+ sharpness: Int32,
+ colour: Int32,
+ tint: Int32,
+ red_gain: Int32,
+ green_gain: Int32,
+ blue_gain: Int32,
+ }
+ {% for name, kind in DEVICE_SETTINGS %}
+ @[Security(Level::Administrator)]
+ def {{name.id}}(value : {{kind}}, **options)
+ {% if kind.resolve == Bool %}
+ state = value ? 1 : 0
+ data = {{name.id.stringify}} == "auto_off_timer" ? Bytes[0x81, state] : state
+ {% elsif kind.resolve == Int32 %}
+ data = value.clamp(0, 100)
+ {% end %}
+ do_send(Command.parse({{name.id.stringify}}), data, **options)
+ end
+ {% end %}
+
+ def do_device_config
+ {% for name, kind in DEVICE_SETTINGS %}
+ %value = setting?({{kind}}, {{name.id.stringify}})
+ {{name.id}}(%value) unless %value.nil?
+ {% end %}
+ end
+
+ enum ResponseStatus
+ Ack = 0x41 # A
+ Nak = 0x4e # N
+ end
+
+ def received(data, task)
+ hex = data.hexstring
+ logger.debug { "Samsung sent: #{hex}" }
+
+ # Verify the checksum of the response
+ if data[-1] != (checksum = data[1..-2].sum(0) & 0xFF)
+ logger.error { "Invalid response, checksum should be: #{checksum.to_s(16)}" }
+ return task.try &.retry
+ end
+
+ status = ResponseStatus.from_value(data[4])
+ command = Command.from_value(data[5])
+ values = data[6..-2]
+ value = values.first
+
+ case status
+ when .ack?
+ case command
+ when .status?
+ self[:hard_off] = hard_off = values[0] == 0
+ self[:power] = false if hard_off
+ self[:volume] = values[1]
+ self[:audio_mute] = values[2] == 1
+ self[:input] = Input.from_value(values[3])
+ check_power_state
+ when .panel_mute?
+ self[:power] = value == 0
+ check_power_state
+ when .volume?
+ self[:volume] = value
+ self[:audio_mute] = false if value > 0
+ when .brightness?
+ self[:brightness] = value
+ when .input?
+ current_input = Input.from_value(value)
+ self[:input] = current_input
+ # The input feedback behaviour seems to go a little odd when
+ # screen split is active. Ignore any input forcing when on.
+ unless self[:screen_split]?.try &.as_bool
+ if current_input == @input_target
+ @input_target = nil
+ elsif input_target = @input_target
+ switch_to(input_target)
+ end
+ end
+ when .speaker?
+ self[:speaker] = SpeakerMode.from_value(value)
+ when .hard_off?
+ unless self[:hard_off]?.try &.as_bool
+ self[:hard_off] = hard_off = value == 0
+ self[:power] = false if hard_off
+ end
+ when .screen_split?
+ self[:screen_split] = value >= 0
+ when .software_version?
+ self[:software_version] = values.join
+ when .serial_number?
+ self[:serial_number] = values.join
+ else
+ logger.debug { "Samsung responded with ACK: #{value}" }
+ end
+
+ task.try &.success
+ when .nak?
+ task.try &.abort("Samsung responded with NAK: #{hex}")
+ else
+ task.try &.retry
+ end
+ end
+
+ private def check_power_state
+ if self[:power]? == @power_target
+ @power_target = nil
+ elsif power_target = @power_target
+ power(power_target)
+ end
+ end
+
+ enum Command : UInt8
+ Status = 0x00
+ HardOff = 0x11 # Completely powers off
+ PanelMute = 0xF9 # Screen blanking / visual mute
+ Volume = 0x12
+ Contrast = 0x24
+ Brightness = 0x25
+ Sharpness = 0x26
+ Colour = 0x27
+ Tint = 0x28
+ RedGain = 0x29
+ GreenGain = 0x2A
+ BlueGain = 0x2B
+ Input = 0x14
+ Mode = 0x18
+ Size = 0x19
+ Pip = 0x3C # picture in picture
+ AutoAdjust = 0x3D
+ WallMode = 0x5C # Video wall mode
+ Safety = 0x5D
+ WallOn = 0x84 # Video wall enabled
+ WallUser = 0x89 # Video wall user control
+ Speaker = 0x68
+ NetworkStandby = 0xB5 # Keep NIC active in standby, enable power on (without WOL)
+ AutoOffTimer = 0xE6 # Eco options (auto power off)
+ AutoPower = 0x33 # Device auto power control (presumably signal based?)
+ ScreenSplit = 0xB2 # Tri / quad split (larger panels only)
+ SoftwareVersion = 0x0E
+ SerialNumber = 0x0B
+ Time = 0xA7
+ Timer = 0xA4
+
+ def build(id : UInt8, data : Bytes) : Bytes
+ Bytes.new(data.size + 5).tap do |bytes|
+ bytes[0] = INDICATOR # Header
+ bytes[1] = self.value # Command
+ bytes[2] = id # Display ID
+ bytes[3] = data.size.to_u8 # Data size
+ data.each_with_index(4) { |b, i| bytes[i] = b } # Data
+ bytes[-1] = (bytes[1..-2].sum(0) & 0xFF).to_u8 # Checksum
+ end
+ end
+ end
+
+ private def do_send(command : Command, data : Int | Bytes = Bytes.empty, **options)
+ data = Bytes[data] if data.is_a?(Int)
+ bytes = command.build(@id, data)
+ logger.debug { "Sending to Samsung: #{bytes.hexstring}" }
+ send(bytes, **options)
+ end
+end
diff --git a/drivers/samsung/displays/mdc_protocol_spec.cr b/drivers/samsung/displays/mdc_protocol_spec.cr
new file mode 100644
index 00000000000..e0a1800b261
--- /dev/null
+++ b/drivers/samsung/displays/mdc_protocol_spec.cr
@@ -0,0 +1,65 @@
+# [header, command, id, data.size, [data], checksum]
+
+DriverSpecs.mock_driver "Samsung::Displays::MDCProtocol" do
+ id = "\x00"
+
+ # connected -> do_poll
+ # power? will take priority over status as status has priority = 0
+ # power? -> panel_mute
+ should_send("\xAA\xF9#{id}\x00\xF9")
+ responds("\xAA\xFF#{id}\x03A\xF9\x00\x3C")
+ status[:power].should eq(true)
+ # status
+ should_send("\xAA\x00#{id}\x00\x00")
+ responds("\xAA\xFF#{id}\x09A\x00\x01\x06\x00\x14\x00\x00\x00\x64")
+ status[:hard_off].should eq(false)
+ status[:power].should eq(true)
+ status[:volume].should eq(6)
+ status[:audio_mute].should eq(false)
+ status[:input].should eq("Vga")
+
+ exec(:volume, 24)
+ should_send("\xAA\x12#{id}\x01\x18\x2B")
+ responds("\xAA\xFF#{id}\x03A\x12\x18\x6D")
+ status[:volume].should eq(24)
+ status[:audio_mute].should eq(false)
+
+ exec(:volume, 6)
+ should_send("\xAA\x12#{id}\x01\x06\x19")
+ responds("\xAA\xFF#{id}\x03A\x12\x06\x5B")
+ status[:volume].should eq(6)
+ status[:audio_mute].should eq(false)
+
+ exec(:mute)
+ # Video mute
+ should_send("\xAA\xF9#{id}\x01\x01\xFB")
+ responds("\xAA\xFF#{id}\x03A\xF9\x01\x3D")
+ status[:power].should eq(false)
+ # Audio mute
+ should_send("\xAA\x12#{id}\x01\x00\x13")
+ responds("\xAA\xFF\x00\x03A\x12\x00\x55")
+ status[:audio_mute].should eq(true)
+ status[:volume].should eq(0)
+
+ exec(:unmute)
+ # Video unmute
+ should_send("\xAA\xF9#{id}\x01\x00\xFA")
+ responds("\xAA\xFF#{id}\x03A\xF9\x00\x3C")
+ status[:power].should eq(true)
+ # Audio unmute
+ should_send("\xAA\x12#{id}\x01\x06\x19")
+ responds("\xAA\xFF#{id}\x03A\x12\x06\x5B")
+ status[:audio_mute].should eq(false)
+ status[:volume].should eq(6)
+
+ exec(:switch_to, "hdmi")
+ should_send("\xAA\x14#{id}\x01\x21\x36")
+ responds("\xAA\xFF#{id}\x03A\x14\x21\x78")
+ status[:input].should eq("Hdmi")
+
+ # power(false) == video_mute(true)
+ exec(:power, false)
+ should_send("\xAA\xF9#{id}\x01\x01\xFB")
+ responds("\xAA\xFF#{id}\x03A\xF9\x01\x3D")
+ status[:power].should eq(false)
+end
diff --git a/drivers/screen_technics/connect.cr b/drivers/screen_technics/connect.cr
new file mode 100644
index 00000000000..51ad1a467fd
--- /dev/null
+++ b/drivers/screen_technics/connect.cr
@@ -0,0 +1,182 @@
+module ScreenTechnics; end
+
+require "placeos-driver/interface/moveable"
+require "placeos-driver/interface/stoppable"
+
+# Documentation: https://aca.im/driver_docs/Screen%20Technics/Screen%20Technics%20IP%20Connect%20module.pdf
+# Default user: Admin
+# Default pass: Connect
+
+class ScreenTechnics::Connect < PlaceOS::Driver
+ include Interface::Moveable
+ include Interface::Stoppable
+
+ # Discovery Information
+ descriptive_name "Screen Technics Projector Screen Control"
+ generic_name :Screen
+ tcp_port 3001
+
+ COMMANDS = {
+ up: 30,
+ down: 33,
+ status: 1, # this differs from the doc, but appears to work
+ stop: 36,
+ }
+
+ CMD_LOOKUP = {
+ 30 => :up,
+ 33 => :down,
+ 1 => :status,
+ 36 => :stop,
+ }
+
+ def on_load
+ # Communication settings
+ queue.delay = 500.milliseconds
+ transport.tokenizer = Tokenizer.new("\r\n")
+
+ on_update
+ end
+
+ def on_update
+ @count = setting?(Int32, :screen_count) || 1
+ end
+
+ def connected
+ schedule.every(15.seconds, immediate: true) {
+ (0...@count).each { |index| query_state(index) }
+ }
+ end
+
+ def disconnected
+ queue.clear
+ schedule.clear
+ end
+
+ def move(position : MoveablePosition, index : Int32 | String = 0)
+ index = index.to_i
+
+ case position
+ when MoveablePosition::Up
+ up(index)
+ when MoveablePosition::Down
+ down(index)
+ else
+ raise "invalid position requested"
+ end
+ end
+
+ def down(index : Int32 = 0)
+ return if down?(index)
+ stop(index)
+ do_send :down, index, name: "direction#{index}"
+ query_state(index)
+ end
+
+ def down?(index : Int32 = 0)
+ {"moving_bottom", "at_bottom"}.includes?(self["screen#{index}"]?)
+ end
+
+ def up(index : Int32 = 0)
+ return if up?(index)
+ stop(index)
+ do_send :up, index, name: "direction#{index}"
+ query_state(index)
+ end
+
+ def up?(index : Int32 = 0)
+ {"moving_top", "at_top"}.includes?(self["screen#{index}"]?)
+ end
+
+ def stop(index : Int32 | String = 0, emergency : Bool = false)
+ index = index.to_i
+
+ do_send(
+ :stop, index,
+ name: "stop#{index}",
+ clear_queue: emergency,
+ priority: emergency ? (queue.priority + 50) : queue.priority
+ )
+ end
+
+ def query_state(index : Int32 = 0)
+ do_send :status, index, 0x20
+ end
+
+ STATUS = {
+ 0 => :moving_top,
+ 1 => :moving_bottom,
+ 2 => :moving_preset_1,
+ 3 => :moving_preset_2,
+ 4 => :moving_top, # preset top
+ 5 => :moving_bottom, # preset bottom
+ 6 => :at_top,
+ 7 => :at_bottom,
+ 8 => :at_preset_1,
+ 9 => :at_preset_2,
+ 10 => :stopped,
+ 11 => :error,
+ # 12 => undefined
+ 13 => :error_timeout,
+ 14 => :error_current,
+ 15 => :error_rattle,
+ 16 => :at_bottom, # preset bottom
+ }
+
+ def received(data, task)
+ data = String.new(data)
+ logger.debug { "Screen sent #{data}" }
+
+ # Builds an array of numbers from the returned string
+ parts = data.split(/,/).map &.strip.to_i
+ cmd = CMD_LOOKUP[parts[0] - 100]?
+
+ if cmd
+ index = parts[2] - 17
+
+ case cmd
+ when :up
+ logger.debug { "Screen#{index} moving up" }
+ self["position#{index}"] = MoveablePosition::Up
+ self["moving#{index}"] = true
+ when :down
+ logger.debug { "Screen#{index} moving down" }
+ self["position#{index}"] = MoveablePosition::Down
+ self["moving#{index}"] = true
+ when :stop
+ logger.debug { "Screen#{index} stopped" }
+ self["moving#{index}"] = false
+ screen = "screen#{index}"
+ self[screen] = :stopped unless {"at_top", "at_bottom"}.includes?(self[screen]?)
+ when :status
+ self["screen#{index}"] = status = STATUS[parts[-1]]
+
+ case status
+ when :moving_top, :at_top
+ self["position#{index}"] = MoveablePosition::Up
+ self["moving#{index}"] = status == :moving_top
+ when :moving_bottom, :at_bottom
+ self["position#{index}"] = MoveablePosition::Down
+ self["moving#{index}"] = status == :moving_bottom
+ when :stopped
+ self["moving#{index}"] = false
+ when :error, :error_timeout, :error_current, :error_rattle
+ self["moving#{index}"] = false
+ end
+ end
+
+ task.try &.success
+ else
+ error = "Unknown command #{parts[0]}"
+ logger.debug { error }
+ task.try &.abort(error)
+ end
+ end
+
+ protected def do_send(cmd, index = 0, *args, **options)
+ address = index + 17
+ parts = {COMMANDS[cmd], address} + args
+ request = "#{parts.join(", ")}\r\n"
+ send request, **options
+ end
+end
diff --git a/drivers/screen_technics/connect_spec.cr b/drivers/screen_technics/connect_spec.cr
new file mode 100644
index 00000000000..8f5bea2e626
--- /dev/null
+++ b/drivers/screen_technics/connect_spec.cr
@@ -0,0 +1,66 @@
+DriverSpecs.mock_driver "ScreenTechnics::Connect" do
+ # On connect it queries the state of all screens
+ should_send("1, 17, 32\r\n")
+ responds("101, 1, 17, 1\r\n")
+
+ status[:position0].should eq("Down")
+ status[:moving0].should be_true
+ status[:screen0].should eq("moving_bottom")
+
+ # Screen Technics requires a large delay between requests
+ # The timeout is because this execute won't occur until after a delay
+ exec(:query_state, index: 1) do |ret_val|
+ should_send("1, 18, 32\r\n", timeout: 1.second)
+ responds("101, 1, 18, 6\r\n")
+
+ # Wait for the execute return value
+ ret_val.get
+
+ status[:position1].should eq("Up")
+ status[:moving1].should eq(false)
+ status[:screen1].should eq("at_top")
+ end
+
+ # ===================
+ # Test emergency stop
+ # ===================
+ exec(:move, "Down", 2) do |ret_val|
+ # A call to down involves a
+ # * stop command
+ # * down command
+ # * status request
+ sleep 1.second
+ should_send("36, 19\r\n", timeout: 1.second)
+ responds("136, 1, 19\r\n")
+
+ # --> Wait for the down command
+ should_send("33, 19\r\n", timeout: 1.second)
+
+ # Execute the emergency stop and request another down request
+ exec(:stop, index: 2, emergency: true) do |response|
+ exec(:move, "Down", 2)
+ sleep 500.milliseconds
+
+ # --> respond to the down command
+ responds("133, 1, 19, 1\r\n")
+
+ # Should receive emergency stop command
+ should_send("36, 19\r\n", timeout: 1.second)
+ responds("136, 1, 19\r\n")
+ status[:moving2].should eq(false)
+ response.get
+ end
+
+ # Original down command should have failed
+ expect_raises(PlaceOS::Driver::RemoteException, "queue cleared (Abort)") do
+ ret_val.get
+ end
+
+ puts "(timeout below expected)"
+
+ # Ensure second down command is not sent
+ expect_raises(Channel::ClosedError) do
+ should_send("33, 17\r\n", timeout: 1.second)
+ end
+ end
+end
diff --git a/drivers/sharp/pn_series.cr b/drivers/sharp/pn_series.cr
new file mode 100644
index 00000000000..25bafd99edf
--- /dev/null
+++ b/drivers/sharp/pn_series.cr
@@ -0,0 +1,264 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+# Documentation: https://aca.im/driver_docs/Sharp/pnl601b.pdf
+# also https://aca.im/driver_docs/Sharp/PN_L802B_operation_guide.pdf
+
+class Sharp::PnSeries < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Input
+ DVI = 1
+ HDMI = 10
+ HDMI2 = 13
+ HDMI3 = 18
+ DisplayPort = 14
+ VGA = 2
+ VGA2 = 16
+ Component = 3
+
+ def data
+ "INPS" + self.value.to_s.rjust(4, '0')
+ end
+ end
+
+ include Interface::InputSelection(Input)
+
+ tcp_port 10008
+ descriptive_name "Sharp Monitor"
+ generic_name :Display
+
+ @volume_min : Int32 = 0
+ @volume_max : Int32 = 31
+ @brightness_min : Int32 = 0
+ @brightness_max : Int32 = 31
+ @contrast_min : Int32 = 0
+ @contrast_max : Int32 = 60 # multiply by two when VGA selected
+ @dbl_contrast : Bool = true
+ @model_number : Bool = false
+
+ @vol_status : PlaceOS::Driver::Proxy::Scheduler::TaskWrapper? = nil
+
+ DELIMITER = "\x0D\x0A"
+
+ def on_load
+ transport.tokenizer = Tokenizer.new(DELIMITER)
+ end
+
+ def connected
+ # Will be sent after login is requested (config - wait ready)
+ send_credentials
+
+ schedule.every(60.seconds) do
+ logger.debug { "-- Polling Display" }
+ do_poll
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool)
+ delay = self[:power_on_delay]?.try(&.as_i) || 5
+
+ # If the requested state is different from the current state
+ if state != !!self[:power]?.try(&.as_bool)
+ if state
+ logger.debug { "-- Sharp LCD, requested to power on" }
+ do_send("POWR 1", name: :POWR, timeout: delay.seconds + 15.seconds)
+ self[:warming] = true
+ self[:power] = true
+ do_send("POWR????", name: :POWR, timeout: 10.seconds) # clears warming
+ else
+ logger.debug { "-- Sharp LCD, requested to power off" }
+ do_send("POWR 0", name: :POWR, timeout: 15.seconds)
+ self[:power] = false
+ end
+ end
+
+ power?
+ mute_status(0)
+ volume_status(0)
+ end
+
+ def power?(**options)
+ do_send("POWR????", **options, name: :POWR, timeout: 10.seconds).get
+ self[:power].as_bool
+ end
+
+ # Resets the brightness and contrast settings
+ def reset
+ do_send("ARST 2")
+ end
+
+ def switch_to(input : Input)
+ logger.debug { "-- Sharp LCD, requested to switch to: #{input}" }
+ do_send(input.data, name: :input, delay: 2.seconds, timeout: 20.seconds).get # does an auto adjust on switch to vga
+ video_input(40)
+ brightness_status(40) # higher status than polling commands - lower than input switching (vid then audio is common)
+ contrast_status(40)
+ end
+
+ AUDIO = {
+ audio1: "ASDP 2",
+ audio2: "ASDP 3",
+ dvi: "ASDP 1",
+ dvi_alt: "ASDA 1",
+ hdmi: "ASHP 0",
+ hdmi_3mm: "ASHP 1",
+ hdmi_rca: "ASHP 2",
+ vga: "ASAP 1",
+ component: "ASCA 1",
+ }
+ AUDIO_RESPONSE = AUDIO.to_h.invert
+
+ def switch_audio(input : String)
+ logger.debug { "-- Sharp LCD, requested to switch audio to: #{input}" }
+
+ do_send(AUDIO[input], name: "audio")
+ mute_status(40) # higher status than polling commands - lower than input switching
+ volume_status(40) # Mute response requests volume
+ end
+
+ def auto_adjust
+ do_send("AGIN 1", timeout: 20.seconds)
+ end
+
+ def brightness(val : Int32)
+ do_send("VLMP#{val.clamp(@brightness_min, @brightness_max).to_s.rjust(4, ' ')}")
+ end
+
+ def contrast(val : Int32)
+ # See Sharp manual
+ multiplier = self[:input]? == "VGA" && @dbl_contrast ? 2 : 1
+ val = val.clamp(@contrast_min, @contrast_max) * multiplier
+ do_send("CONT#{val.to_s.rjust(4, ' ')}")
+ end
+
+ def volume(val : Int32)
+ @vol_status.try(&.cancel)
+ @vol_status = schedule.in(2.seconds) do
+ @vol_status = nil
+ volume_status
+ end
+ do_send("VOLM#{val.clamp(@volume_min, @volume_max).to_s.rjust(4, ' ')}")
+ end
+
+ # There seems to only be audio mute available
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ if layer == MuteLayer::Video
+ logger.warn { "Sharp LCD requested to mute video which is unsupported" }
+ else
+ logger.debug { "Sharp LCD, requested to mute #{state}" }
+ do_send("MUTE #{state ? '1' : '0'}")
+ mute_status(50) # High priority mute status
+ end
+ end
+
+ OPERATION_CODE = {
+ video_input: "INPS",
+ volume_status: "VOLM",
+ mute_status: "MUTE",
+ power_on_delay: "PWOD",
+ contrast_status: "CONT",
+ brightness_status: "VLMP",
+ model_number: "INF1",
+ }
+ {% for name, cmd in OPERATION_CODE %}
+ @[Security(Level::Administrator)]
+ def {{name.id}}(priority : Int32 = 0, **options)
+ data = {{cmd.id.stringify}} + "????"
+ logger.debug { "Sharp sending: #{data}" }
+ do_send(data, **options, priority: priority) # Status polling is a low priority
+ end
+ {% end %}
+
+ def do_poll
+ if power?
+ model_number unless self[:model_number]? # only query the model number if we don't already have it
+ power_on_delay
+ mute_status
+ end
+ end
+
+ private def determine_contrast_mode
+ # As of 09/2015 only the PN-L802B does not have double contrast on RGB input.
+ # All prior models do double the contrast and don't have an L so let's assume it's the L in the model number that determines this for now
+ # (we can confirm the logic as more models are released)
+ @dbl_contrast = false if self[:model_number].as_s.includes?('L')
+ logger.debug { "dbl_contrast is #{@dbl_contrast}" }
+ end
+
+ private def send_credentials
+ do_send(setting?(String?, :username) || "", priority: 100, delay: 500.milliseconds) # , wait: false)
+ # TODO: figure out equivalent in crystal for delay_on_receive
+ do_send(setting?(String?, :password) || "", priority: 100) # , delay_on_receive: 1000)
+ end
+
+ def received(data, task)
+ data = String.new(data[0..-3])
+ logger.debug { "-- Sharp LCD, received: #{data}" }
+
+ if data == "Password:OK"
+ return task.try(&.success("Login successful"))
+ elsif data == "Password:Login incorrect"
+ schedule.in(5.seconds) { send_credentials }
+ return task.try(&.success("Sharp LCD, bad login or logged off. Attempting login.."))
+ elsif data == "OK"
+ return task.try(&.success)
+ elsif data == "WAIT"
+ logger.debug { "-- Sharp LCD, wait" }
+ return
+ elsif data == "ERR"
+ return task.try(&.abort("-- Sharp LCD, error"))
+ elsif data.size < 8 # Out of order send?
+ return task.try(&.abort("Sharp sent out of order response: #{data}"))
+ end
+
+ command, value = data.split
+
+ case command
+ when "POWR" # Power status
+ self[:warming] = false
+ self[:power] = value.to_i > 0
+ when "INPS" # Input status
+ input = Input.from_value?(value.to_i)
+ self[:input] = input || "unknown"
+ logger.debug { "-- Sharp LCD, input #{self[:input]} == #{value}" }
+ when "VOLM" # Volume status
+ self[:volume] = value.to_i unless self[:audio_mute]?.try(&.as_bool)
+ when "MUTE" # Mute status
+ self[:audio_mute] = (mute = value.to_i == 1)
+ if mute
+ self[:volume] = 0
+ else
+ volume_status(90) # high priority
+ end
+ when "CONT" # Contrast status
+ self[:contrast] = value.to_i / (self[:input]? == "VGA" && @dbl_contrast ? 2 : 1)
+ when "VLMP" # brightness status
+ self[:brightness] = value.to_i
+ when "PWOD"
+ self[:power_on_delay] = value.to_i
+ when "INF1"
+ self[:model_number] = value
+ logger.debug { "-- Sharp LCD, model number #{self[:model_number]}" }
+ determine_contrast_mode
+ when "ASDP", "ASDA", "ASHP", "ASAP", "ASCA" # audio switching commands
+ self[:audio_input] = AUDIO_RESPONSE[data] || "unknown"
+ end
+
+ task.try(&.success)
+ end
+
+ private def do_send(data, delay = 100.milliseconds, **options)
+ send("#{data}#{DELIMITER}", **options, delay: delay)
+ end
+end
diff --git a/drivers/sharp/pn_series_spec.cr b/drivers/sharp/pn_series_spec.cr
new file mode 100644
index 00000000000..4a268fa8a46
--- /dev/null
+++ b/drivers/sharp/pn_series_spec.cr
@@ -0,0 +1,75 @@
+DriverSpecs.mock_driver "Sharp::PnSeries" do
+ # connected
+ # send_credentials
+ should_send("\x0D\x0A")
+ responds("OK\x0D\x0A")
+ should_send("\x0D\x0A")
+ responds("Password:Login incorrect\x0D\x0A")
+
+ # Settings can only be accessed after on_load and connected
+ settings({
+ username: "user",
+ password: "pass",
+ })
+
+ # Retrying send_credentials
+ sleep 5
+ should_send("user\x0D\x0A")
+ responds("OK\x0D\x0A")
+ should_send("pass\x0D\x0A")
+ responds("Password:OK\x0D\x0A")
+
+ exec(:do_poll)
+ should_send("POWR????\x0D\x0A")
+ responds("POWR 001\x0D\x0A")
+ status[:warming].should eq(false)
+ status[:power].should eq(true)
+ should_send("INF1????\x0D\x0A")
+ responds("INF1 P802B\x0D\x0A")
+ status[:model_number].should eq("P802B")
+ should_send("PWOD????\x0D\x0A")
+ responds("PWOD 002\x0D\x0A")
+ status[:power_on_delay].should eq(2)
+ should_send("MUTE????\x0D\x0A")
+ responds("MUTE 000\x0D\x0A")
+ status[:audio_mute].should eq(false)
+ should_send("VOLM????\x0D\x0A")
+ responds("VOLM 010\x0D\x0A")
+ status[:volume].should eq(10)
+
+ exec(:switch_to, "hdmi")
+ should_send("INPS0010\x0D\x0A")
+ responds("WAIT\x0D\x0A")
+ responds("OK\x0D\x0A")
+ sleep 2
+ should_send("INPS????\x0D\x0A")
+ responds("INPS 10\x0D\x0A")
+ status[:input].should eq("HDMI")
+ should_send("VLMP????\x0D\x0A")
+ responds("VLMP 15\x0D\x0A")
+ status[:brightness].should eq(15)
+ should_send("CONT????\x0D\x0A")
+ responds("CONT 20\x0D\x0A")
+ status[:contrast].should eq(20)
+
+ exec(:switch_audio, "component")
+ should_send("ASCA 1\x0D\x0A")
+ responds("ASCA 1\x0D\x0A")
+ status[:audio_input].should eq("component")
+
+ exec(:volume, 100)
+ should_send("VOLM 31\x0D\x0A")
+ responds("ASCA 1\x0D\x0A")
+
+ exec(:power, false)
+ should_send("POWR 0\x0D\x0A")
+ responds("OK\x0D\x0A")
+ should_send("POWR????\x0D\x0A")
+ responds("POWR 0\x0D\x0A")
+ status[:warming].should eq(false)
+ status[:power].should eq(false)
+ should_send("MUTE????\x0D\x0A")
+ responds("MUTE 1\x0D\x0A")
+ status[:audio_mute].should eq(true)
+ status[:volume].should eq(0)
+end
diff --git a/drivers/sony/camera/cgi_protocol.cr b/drivers/sony/camera/cgi_protocol.cr
new file mode 100644
index 00000000000..d5a20e2a4ff
--- /dev/null
+++ b/drivers/sony/camera/cgi_protocol.cr
@@ -0,0 +1,316 @@
+require "placeos-driver/interface/camera"
+
+module Sony; end
+
+module Sony::Camera; end
+
+# Documentation: https://aca.im/driver_docs/Sony/sony-camera-CGI-Commands-1.pdf
+
+class Sony::Camera::CGI < PlaceOS::Driver
+ # include Interface::Powerable
+ include Interface::Camera
+
+ # Discovery Information
+ generic_name :Camera
+ descriptive_name "Sony Camera HTTP CGI Protocol"
+
+ default_settings({
+ basic_auth: {
+ username: "admin",
+ password: "Admin_1234",
+ },
+ invert_controls: false,
+ presets: {
+ name: {pan: 1, tilt: 1, zoom: 1},
+ },
+ })
+
+ enum Movement
+ Idle
+ Moving
+ Unknown
+ end
+
+ def on_load
+ # Configure the constants
+ @pantilt_speed = -100..100
+ self[:pan_speed] = self[:tilt_speed] = {min: -100, max: 100, stop: 0}
+ self[:has_discrete_zoom] = true
+
+ schedule.every(60.seconds) { query_status }
+ schedule.in(5.seconds) do
+ query_status
+ info?
+ end
+ on_update
+ end
+
+ @invert_controls = false
+ @presets = {} of String => NamedTuple(pan: Int32, tilt: Int32, zoom: Int32)
+
+ def on_update
+ self[:invert_controls] = @invert_controls = setting?(Bool, :invert_controls) || false
+ @presets = setting?(Hash(String, NamedTuple(pan: Int32, tilt: Int32, zoom: Int32)), :presets) || {} of String => NamedTuple(pan: Int32, tilt: Int32, zoom: Int32)
+ self[:presets] = @presets.keys
+ end
+
+ # 24bit twos complement
+ private def twos_complement(value)
+ if value > 0
+ value > 0x80000 ? -(((~(value & 0xFFFFF)) + 1) & 0xFFFFF) : value
+ else
+ ((~(-value & 0xFFFFF)) + 1) & 0xFFFFF
+ end
+ end
+
+ private def query(path, **opts, &block : Hash(String, String) -> _)
+ queue(**opts) do |task|
+ response = get(path)
+ data = response.body.not_nil!
+
+ raise "unexpected response #{response.status_code}\n#{response.body}" unless response.success?
+
+ # convert data into more consumable state
+ state = {} of String => String
+ data.split("&").each do |key_value|
+ parts = key_value.strip.split("=")
+ state[parts[0]] = parts[1]
+ end
+
+ result = block.call(state)
+ task.success result
+ end
+ end
+
+ # Temporary values until the camera is queried
+ @moving = false
+ @zooming = false
+ @max_speed = 1
+
+ def query_status(priority : Int32 = 0)
+ # Response looks like:
+ # AbsolutePTZF=15400,fd578,0000,cbde&PanMovementRange=eac00,15400
+ query("/command/inquiry.cgi?inq=ptzf", priority: priority) do |response|
+ # load the current state
+ response.each do |key, value|
+ case key
+ when "AbsolutePTZF"
+ # Pan, Tilt, Zoom,Focus
+ # AbsolutePTZF=15400,fd578,0000,ca52
+ parts = value.split(",")
+ self[:pan] = @pan = twos_complement parts[0].to_i(16)
+ self[:tilt] = @tilt = twos_complement parts[1].to_i(16)
+ self[:zoom] = @zoom = parts[2].to_i(16)
+ when "PanMovementRange"
+ # PanMovementRange=eac00,15400
+ parts = value.split(",")
+ pan_min = twos_complement parts[0].to_i(16)
+ pan_max = twos_complement parts[1].to_i(16)
+ @pan_range = pan_min..pan_max
+ self[:pan_range] = {min: pan_min, max: pan_max}
+ when "TiltMovementRange"
+ # TiltMovementRange=fc400,b400
+ parts = value.split(",")
+ tilt_min = twos_complement parts[0].to_i(16)
+ tilt_max = twos_complement parts[1].to_i(16)
+ @tilt_range = tilt_min..tilt_max
+ self[:tilt_range] = {min: tilt_min, max: tilt_max}
+ when "ZoomMovementRange"
+ # min, max, digital
+ # ZoomMovementRange=0000,4000,7ac0
+ parts = value.split(",")
+ zoom_min = parts[0].to_i(16)
+ zoom_max = parts[1].to_i(16)
+ @zoom_range = zoom_min..zoom_max
+ self[:zoom_range] = {min: zoom_min, max: zoom_max}
+ when "PtzfStatus"
+ # PtzfStatus=idle,idle,idle,idle
+ parts = value.split(",").map { |state| Movement.parse(state) }[0..2]
+ self[:moving] = @moving = parts.includes?(Movement::Moving)
+
+ # when "AbsoluteZoom"
+ # # AbsoluteZoom=609
+ # self[:zoom] = @zoom = value.to_i(16)
+
+ # NOTE:: These are not required as speeds are scaled
+ #
+ # when "ZoomMaxVelocity"
+ # # ZoomMaxVelocity=8
+ # @zoom_speed = 1..value.to_i(16)
+
+ when "PanTiltMaxVelocity"
+ # PanTiltMaxVelocity=24
+ @max_speed = value.to_i(16)
+ end
+ end
+
+ response
+ end
+ end
+
+ def info?
+ query("/command/inquiry.cgi?inq=system", priority: 0) do |response|
+ response.each do |key, value|
+ if {"ModelName", "Serial", "SoftVersion", "ModelForm", "CGIVersion"}.includes?(key)
+ self[key.underscore] = value
+ end
+ end
+ response
+ end
+ end
+
+ private def action(path, **opts, &block : HTTP::Client::Response -> _)
+ queue(**opts) do |task|
+ response = get(path)
+ raise "request error #{response.status_code}\n#{response.body}" unless response.success?
+
+ result = block.call(response)
+ task.success result
+ end
+ end
+
+ # Implement Stoppable interface
+ def stop(index : Int32 | String = 0, emergency : Bool = false)
+ # indexes start at 1 on sony cameras
+ index = index.to_i + 1
+
+ action("/command/ptzf.cgi?Move=stop,motor,image#{index}",
+ priority: 999,
+ name: "moving",
+ clear_queue: emergency
+ ) do
+ zoom ZoomDirection::Stop if @zooming
+ self[:moving] = @moving = false
+ query_status
+ end
+ end
+
+ # Implement Moveable interface
+ def move(position : MoveablePosition, index : Int32 | String = 0)
+ # indexes start at 1 on sony cameras
+ index = index.to_i + 1
+
+ case position
+ when MoveablePosition::Up, MoveablePosition::Down,
+ MoveablePosition::Left, MoveablePosition::Right
+ # Tilt, Pan
+ if @invert_controls && (position.up? || position.down?)
+ position = position.up? ? MoveablePosition::Down : MoveablePosition::Up
+ end
+
+ action("/command/ptzf.cgi?Move=#{position.to_s.downcase},0,image#{index}",
+ name: "moving"
+ ) { self[:moving] = @moving = true }
+ when MoveablePosition::In
+ zoom ZoomDirection::In
+ when MoveablePosition::Out
+ zoom ZoomDirection::Out
+ else
+ raise "unsupported direction: #{position}"
+ end
+ end
+
+ macro in_range(range, value)
+ {{value}} = if {{range}}.includes? {{value}}
+ {{value}}
+ else
+ {{value}} < {{range}}.begin ? {{range}}.begin : {{range}}.end
+ end
+ {{value}} = twos_complement({{value}})
+ end
+
+ def pantilt(pan : Int32, tilt : Int32, zoom : Int32? = nil) : Nil
+ in_range @pan_range, pan
+ in_range @tilt_range, tilt
+
+ if zoom
+ in_range @zoom_range, zoom
+
+ action("/command/ptzf.cgi?AbsolutePTZF=#{pan.to_s(16)},#{tilt.to_s(16)},#{zoom.to_s(16)}",
+ name: "position"
+ ) do
+ self[:pan] = @pan = pan
+ self[:tilt] = @tilt = tilt
+ self[:zoom] = @zoom = zoom.not_nil!
+ end
+ else
+ action("/command/ptzf.cgi?AbsolutePanTilt=#{pan.to_s(16)},#{tilt.to_s(16)},#{@max_speed.to_s(16)}",
+ name: "position"
+ ) do
+ self[:pan] = @pan = pan
+ self[:tilt] = @tilt = tilt
+ end
+ end
+ end
+
+ # Implement Camera interface
+ def joystick(pan_speed : Int32, tilt_speed : Int32, index : Int32 | String = 0)
+ index = index.to_i + 1
+ range = -100..100
+ in_range range, pan_speed
+ in_range range, tilt_speed
+
+ tilt_speed = -tilt_speed if @invert_controls && tilt_speed != 0
+
+ action("/command/ptzf.cgi?ContinuousPanTiltZoom=#{pan_speed.to_s(16)},#{tilt_speed.to_s(16)},0,image#{index}",
+ name: "moving"
+ ) do
+ self[:moving] = @moving = (pan_speed != 0 || tilt_speed != 0)
+ query_status if !@moving
+ @moving
+ end
+ end
+
+ def zoom_to(position : Int32, auto_focus : Bool = true, index : Int32 | String = 0)
+ index = index.to_i + 1
+
+ in_range @zoom_range, position
+ action("/command/ptzf.cgi?AbsoluteZoom=#{position.to_s(16)}",
+ name: "zooming"
+ ) { self[:zoom] = @zoom = position }
+ end
+
+ def zoom(direction : ZoomDirection, index : Int32 | String = 0)
+ index = index.to_i + 1
+
+ if direction.stop?
+ action("/command/ptzf.cgi?Move=stop,zoom,image#{index}",
+ priority: 999,
+ name: "zooming"
+ ) { self[:zooming] = @zooming = false }
+ else
+ action("/command/ptzf.cgi?Move=#{direction.out? ? "wide" : "near"},0,image#{index}",
+ name: "zooming"
+ ) { self[:zooming] = @zooming = true }
+ end
+ end
+
+ def home
+ action("/command/presetposition.cgi?HomePos=ptz-recall",
+ name: "position"
+ ) { query_status }
+ end
+
+ def recall(position : String, index : Int32 | String = 0)
+ preset = @presets[position]?
+ if preset
+ pantilt **preset
+ else
+ raise "unknown preset #{position}"
+ end
+ end
+
+ def save_position(name : String, index : Int32 | String = 0)
+ @presets[name] = {
+ pan: @pan, tilt: @tilt, zoom: @zoom,
+ }
+ # TODO:: persist this to the database
+ self[:presets] = @presets.keys
+ end
+
+ def delete_position(name : String, index : Int32 | String = 0)
+ @presets.delete name
+ # TODO:: persist this to the database
+ self[:presets] = @presets.keys
+ end
+end
diff --git a/drivers/sony/camera/cgi_protocol_spec.cr b/drivers/sony/camera/cgi_protocol_spec.cr
new file mode 100644
index 00000000000..755d7ff0b2e
--- /dev/null
+++ b/drivers/sony/camera/cgi_protocol_spec.cr
@@ -0,0 +1,18 @@
+DriverSpecs.mock_driver "Floorsense::Desks" do
+ # Send the request
+ retval = exec(:query_status)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response.output.puts %(AbsolutePTZF=15400,fd578,0000,cb5a&PanMovementRange=eac00,15400&PanPanoramaRange=de00,2200&PanTiltMaxVelocity=24&PtzInstance=1&TiltMovementRange=fc400,b400&TiltPanoramaRange=fc00,1200&ZoomMaxVelocity=8&ZoomMovementRange=0000,4000,7ac0&PtzfStatus=idle,idle,idle,idle&AbsoluteZoom=609)
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.not_nil!["AbsoluteZoom"].should eq("609")
+ status[:pan].should eq(87040)
+ status[:pan_range].should eq({"min" => -87040, "max" => 87040})
+
+ status[:tilt].should eq(-10888)
+ status[:tilt_range].should eq({"min" => -15360, "max" => 46080})
+end
diff --git a/drivers/sony/displays/bravia.cr b/drivers/sony/displays/bravia.cr
new file mode 100644
index 00000000000..519b5fe6ede
--- /dev/null
+++ b/drivers/sony/displays/bravia.cr
@@ -0,0 +1,240 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+class Sony::Displays::Bravia < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ private INDICATOR = "\x2A\x53" # *S
+ private HASH = "################"
+
+ # Discovery Information
+ tcp_port 20060
+ descriptive_name "Sony Bravia LCD Display"
+ generic_name :Display
+
+ enum Input : UInt32
+ {% for idx in 0..3 %}
+ Tv{{idx}} = {{ idx }}
+ Hdmi{{idx}} = {{10000_0000 + idx}}
+ Mirror{{idx}} = {{50000_0000 + idx}}
+ Vga{{idx}} = {{60000_0000 + idx}}
+ {% end %}
+
+ def self.from_param(value : String) : self
+ from_value UInt32.new(value)
+ rescue
+ raise "Unknown enum #{self} value: #{value}"
+ end
+
+ def to_param : String
+ value.to_s.rjust(5, '0')
+ end
+ end
+
+ include Interface::InputSelection(Input)
+
+ def switch_to(input : Input)
+ logger.debug { "switching input to #{input}" }
+ request(Command::Input, input.to_param)
+ self[:input] = input.to_s
+ input?
+ end
+
+ def input?
+ query(Command::Input, priority: 0)
+ end
+
+ def on_load
+ self[:volume_min] = 0
+ self[:volume_max] = 100
+ end
+
+ def connected
+ schedule.every(30.seconds, true) do
+ do_poll
+ end
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool)
+ request(Command::Power, state)
+ power?
+ end
+
+ def power?
+ query(Command::Power)
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ request(Command::Mute, state)
+ mute?
+ end
+
+ def unmute
+ mute false
+ end
+
+ def mute?
+ query(Command::Mute, priority: 0)
+ end
+
+ def mute_audio(state : Bool = true)
+ request(Command::AudioMute, state)
+ audio_mute?
+ end
+
+ def unmute_audio
+ mute_audio false
+ end
+
+ def audio_mute?
+ query(Command::AudioMute, priority: 0)
+ end
+
+ def volume(level : Int32)
+ request(Command::Volume, level.to_i)
+ volume?
+ end
+
+ def volume?
+ query(Command::Volume, priority: 0)
+ end
+
+ def do_poll
+ if self[:power]?
+ input?
+ mute?
+ audio_mute?
+ volume?
+ end
+ end
+
+ enum MessageType : UInt8
+ Answer = 0x41
+ Control = 0x43
+ Enquiry = 0x45
+ Notify = 0x4e
+ Error = 0x46
+
+ def control_character
+ value.chr
+ end
+ end
+
+ def received(data, task)
+ parsed_data = convert_binary(data[3..6])
+ cmd = Command.from_response?(parsed_data)
+ return task.try(&.abort("unrecognised command: #{parsed_data}")) if cmd.nil?
+ param = data[7..-1]
+ return task.try(&.abort("error")) if param.first? == MessageType::Error.value
+ case MessageType.from_value?(data[2])
+ when MessageType::Answer
+ update_status cmd, param
+ task.try &.success
+ when MessageType::Notify
+ update_status cmd, param
+ else
+ logger.debug { "Unhandled device response: #{data[2].chr rescue data[2]}" }
+ task.try &.abort("Unhandled device response")
+ end
+ end
+
+ COMMANDS = {
+ ir_code: "IRCC",
+ power: "POWR",
+ volume: "VOLU",
+ audio_mute: "AMUT",
+ mute: "PMUT",
+ channel: "CHNN",
+ tv_input: "ISRC",
+ input: "INPT",
+ toggle_mute: "TPMU",
+ pip: "PIPI",
+ toggle_pip: "TPIP",
+ position_pip: "TPPP",
+ broadcast_address: "BADR",
+ mac_address: "MADR",
+ }
+
+ {% begin %}
+ enum Command
+ {% begin %}
+ {% for command in COMMANDS.keys %}
+ {{ command.camelcase.id }}
+ {% end %}
+ {% end %}
+
+ def function
+ {% begin %}
+ case self
+ {% for kv in COMMANDS.to_a %}
+ {% command, value = kv[0], kv[1] %}
+ in {{ command.camelcase }} then {{ value }}
+ {% end %}
+ end
+ {% end %}
+ end
+
+ def self.from_response?(message)
+ {% begin %}
+ case message
+ {% for kv in COMMANDS.to_a %}
+ {% command, value = kv[0], kv[1] %}
+ when {{ value }} then {{ command.camelcase.id }}
+ {% end %}
+ end
+ {% end %}
+ end
+ end
+ {% end %}
+
+ protected def convert_binary(data)
+ String.new(slice: data)
+ end
+
+ protected def request(command, parameter, **options)
+ cmd = command.function
+ parameter = parameter ? 1 : 0 if parameter.is_a?(Bool)
+ param = parameter.to_s.rjust(16, '0')
+ do_send(MessageType::Control, cmd, param, **options)
+ end
+
+ protected def query(state, **options)
+ cmd = state.function
+ do_send(MessageType::Enquiry, cmd, HASH, **options)
+ end
+
+ protected def do_send(type, command, parameter, **options)
+ cmd = "#{INDICATOR}#{type.control_character}#{command}#{parameter}\n"
+ send(cmd, **options)
+ end
+
+ protected def update_status(cmd : Command, param)
+ parsed_data = convert_binary(param)
+ case cmd
+ when .power?
+ self[:power] = parsed_data.to_i == 1
+ when .mute?
+ self[:mute] = parsed_data.to_i == 1
+ when .audio_mute?
+ self[:audio_mute] = parsed_data.to_i == 1
+ when .pip?
+ self[:pip] = parsed_data.to_i == 1
+ when .volume?
+ self[:volume] = parsed_data.to_i
+ when .mac_address?
+ self[:mac_address] = parsed_data.split('#')[0]
+ when .input?
+ self[:input] = Input.from_param(parsed_data[7..15])
+ end
+ end
+end
diff --git a/drivers/sony/displays/bravia_spec.cr b/drivers/sony/displays/bravia_spec.cr
new file mode 100644
index 00000000000..1230faf3afc
--- /dev/null
+++ b/drivers/sony/displays/bravia_spec.cr
@@ -0,0 +1,53 @@
+DriverSpecs.mock_driver "Sony::Displays::Bravia" do
+ exec(:power, true)
+ should_send("\x2A\x53\x43POWR0000000000000001\n")
+ responds("\x2A\x53\x41POWR0000000000000000\n")
+ should_send("\x2A\x53\x45POWR################\n")
+ responds("\x2A\x53\x41POWR0000000000000001\n")
+ status[:power].should eq(true)
+
+ exec(:switch_to, "hdmi1")
+ should_send("\x2A\x53\x43INPT0000000100000001\n")
+ responds("\x2A\x53\x41INPT0000000000000000\n")
+ should_send("\x2A\x53\x45INPT################\n")
+ responds("\x2A\x53\x41INPT0000000100000001\n")
+ status[:input].should eq("Hdmi1")
+
+ exec(:switch_to, "vga3")
+ should_send("\x2A\x53\x43INPT0000000600000003\n")
+ responds("\x2A\x53\x41INPT0000000000000000\n")
+ should_send("\x2A\x53\x45INPT################\n")
+ responds("\x2A\x53\x41INPT0000000600000003\n")
+ status[:input].should eq("Vga3")
+
+ exec(:volume, 99)
+ should_send("\x2A\x53\x43VOLU0000000000000099\n")
+ responds("\x2A\x53\x41VOLU0000000000000000\n")
+ should_send("\x2A\x53\x45VOLU################\n")
+ responds("\x2A\x53\x41VOLU0000000000000099\n")
+ status[:volume].should eq(99)
+
+ # Test failure
+ exec(:mute)
+ should_send("\x2A\x53\x43PMUT0000000000000001\n")
+ responds("\x2A\x53\x41PMUT0000000000000000\n")
+ should_send("\x2A\x53\x45PMUT################\n")
+ responds("\x2A\x53\x41PMUT0000000000000001\n")
+ status[:mute].should eq(true)
+
+ exec(:unmute)
+ should_send("\x2A\x53\x43PMUT0000000000000000\n")
+ responds("\x2A\x53\x41PMUTFFFFFFFFFFFFFFFF\n")
+ should_send("\x2A\x53\x45PMUT################\n")
+ responds("\x2A\x53\x41PMUT0000000000000001\n")
+ status[:mute].should eq(true)
+
+ exec(:volume, 50)
+ should_send("\x2A\x53\x43VOLU0000000000000050\n")
+ responds("\x2A\x53\x4EPMUT0000000000000000\n") # mix in a notify
+ responds("\x2A\x53\x41VOLU0000000000000000\n")
+ should_send("\x2A\x53\x45VOLU################\n")
+ responds("\x2A\x53\x41VOLU0000000000000050\n")
+ status[:volume].should eq(50)
+ status[:mute].should eq(false)
+end
diff --git a/drivers/sony/projector/fh.cr b/drivers/sony/projector/fh.cr
new file mode 100644
index 00000000000..e47aaea7819
--- /dev/null
+++ b/drivers/sony/projector/fh.cr
@@ -0,0 +1,170 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+# Documentation: https://drive.google.com/a/room.tools/file/d/1C0gAWNOtkbrHFyky_9LfLCkPoMcYU9lO/view?usp=sharing
+
+class Sony::Projector::Fh < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ enum Inputs
+ HDMI
+ DVI
+ Video
+ SVideo
+ RGB
+ HDBaseT
+ InputA
+ InputB
+ InputC
+ InputD
+ InputE
+
+ def to_message : String
+ case self
+ in HDMI, DVI, Video, SVideo, RGB, HDBaseT
+ to_s.downcase + "1"
+ in InputA, InputB, InputC, InputD, InputE
+ to_s.underscore
+ end
+ end
+
+ def readable : String
+ to_s.downcase
+ end
+ end
+
+ include PlaceOS::Driver::Interface::InputSelection(Inputs)
+
+ descriptive_name "Sony Projector FH Series"
+ generic_name :Display
+
+ def on_load
+ transport.tokenizer = Tokenizer.new("\r\n")
+ end
+
+ def connected
+ schedule.every(60.seconds) { do_poll }
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool)
+ set("power", state ? "on" : "off").get
+ self[:power] = state
+ end
+
+ def power?
+ get("power_status")
+ !!self[:power]?.try(&.as_bool)
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ set("blank", state ? "on" : "off").get
+ self[:mute] = state
+ end
+
+ def mute?
+ get("blank").get
+ self[:mute].as_bool
+ end
+
+ INPUTS_LOOKUP = {
+ "hdmi1" => Inputs::HDMI,
+ "dvi1" => Inputs::DVI,
+ "video1" => Inputs::Video,
+ "svideo1" => Inputs::SVideo,
+ "rgb1" => Inputs::RGB,
+ "hdbaset1" => Inputs::HDBaseT,
+ "input_a" => Inputs::InputA,
+ "input_b" => Inputs::InputB,
+ "input_c" => Inputs::InputC,
+ "input_d" => Inputs::InputD,
+ "input_e" => Inputs::InputE,
+ }
+
+ def switch_to(input : Inputs)
+ set("input", input.to_message).get
+ self[:input] = input.readable
+ end
+
+ def input?
+ get("input").get
+ self[:input].as_s
+ end
+
+ {% for name in ["contrast", "brightness", "color", "hue", "sharpness"] %}
+ def {{name.id}}?
+ get({{name.id.stringify}})
+ end
+
+ def {{name.id}}(val : Int32)
+ set({{name.id.stringify}}, val.clamp(0, 100))
+ end
+ {% end %}
+
+ private def do_poll
+ return unless power?
+ input?
+ mute?
+ end
+
+ def received(response, task)
+ process_response(response, task)
+ end
+
+ private def process_response(response, task, path = nil)
+ response = String.new(response)
+ logger.debug { "Sony proj sent: #{response}" }
+ data = shellsplit(response.strip.downcase)
+
+ return task.try &.success if data[0] == "ok"
+ return task.try &.abort if data[0] == "err_cmd"
+
+ case path
+ when "power_status"
+ self[:power] = data[0] == "on"
+ when "blank"
+ self[:mute] = data[0] == "on"
+ when "input"
+ self[:input] = INPUTS_LOOKUP[data[0]].readable
+ end
+ task.try &.success
+ end
+
+ private def get(path, **options)
+ cmd = "#{path} ?\r\n"
+ logger.debug { "Sony projector FH requesting: #{cmd}" }
+ send(cmd, **options) { |data, task| process_response(data, task, path) }
+ end
+
+ private def set(path, arg, **options)
+ cmd = "#{path} \"#{arg}\"\r\n"
+ logger.debug { "Sony projector FH sending: #{cmd}" }
+ send(cmd, **options) { |data, task| process_response(data, task, path) }
+ end
+
+ # Quick dirty port of https://github.com/ruby/ruby/blob/master/lib/shellwords.rb
+ private def shellsplit(line : String) : Array(String)
+ words = [] of String
+ field = ""
+ pattern = /\G\s*(?>([^\s\\\'\"]+)|'([^\']*)'|"((?:[^\"\\]|\\.)*)"|(\\.?)|(\S))(\s|\z)?/m
+ line.scan(pattern) do |match|
+ _, word, sq, dq, esc, garbage, sep = match.to_a
+ raise ArgumentError.new("Unmatched quote: #{line.inspect}") if garbage
+ field += (word || sq || dq.try(&.gsub(/\\([$`"\\\n])/, "\\1")) || esc.not_nil!.gsub(/\\(.)/, "\\1"))
+ if sep
+ words << field
+ field = ""
+ end
+ end
+ words
+ end
+end
diff --git a/drivers/sony/projector/fh_spec.cr b/drivers/sony/projector/fh_spec.cr
new file mode 100644
index 00000000000..8b79a742333
--- /dev/null
+++ b/drivers/sony/projector/fh_spec.cr
@@ -0,0 +1,31 @@
+DriverSpecs.mock_driver "Sony::Projector::Fh" do
+ exec(:power?)
+ should_send("power_status ?\r\n")
+ responds("\"standby\"\r\n")
+ status[:power].should eq(false)
+
+ exec(:power, true)
+ should_send("power \"on\"\r\n")
+ responds("ok\r\n")
+ status[:power].should eq(true)
+
+ exec(:mute?)
+ should_send("blank ?\r\n")
+ responds("\"on\"\r\n")
+ status[:mute].should eq(true)
+
+ exec(:mute, false)
+ should_send("blank \"off\"\r\n")
+ responds("ok\r\n")
+ status[:mute].should eq(false)
+
+ exec(:input?)
+ should_send("input ?\r\n")
+ responds("\"hdmi1\"\r\n")
+ status[:input].should eq("hdmi")
+
+ exec(:switch_to, "rgb")
+ should_send("input \"rgb1\"\r\n")
+ responds("ok\r\n")
+ status[:input].should eq("rgb")
+end
diff --git a/drivers/sony/projector/pj_talk.cr b/drivers/sony/projector/pj_talk.cr
new file mode 100644
index 00000000000..dbeee2950f5
--- /dev/null
+++ b/drivers/sony/projector/pj_talk.cr
@@ -0,0 +1,291 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+# Documentation: https://aca.im/driver_docs/Sony/Sony_Q004_R1_protocol.pdf
+# also https://aca.im/driver_docs/Sony/TCP_CMDs.pdf
+
+class Sony::Projector::PjTalk < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ descriptive_name "Sony Projector PjTalk"
+ generic_name :Display
+ tcp_port 53484
+
+ default_settings({
+ community: "SONY",
+ })
+
+ @community : String = ""
+
+ def on_load
+ # abstract tokenizer, expects us to return the message length
+ transport.tokenizer = Tokenizer.new do |io|
+ bytes = io.to_slice
+
+ # Min message length is 10 bytes, with the 10th byte being the payload size
+ bytes.size < 10 ? -1 : 10 + bytes[9]
+ end
+
+ on_update
+ end
+
+ def on_update
+ @community = setting?(String, :community) || "SONY"
+ end
+
+ def connected
+ schedule.every(60.seconds) { do_poll }
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool)
+ if state
+ # Need to send twice in case of deep sleep
+ logger.debug { "requested to power on" }
+ do_send(:set, :power_on, name: :power)
+ else
+ logger.debug { "requested to power off" }
+ do_send(:set, :power_off, name: :power, delay: 3.seconds)
+ end
+
+ # Request status update
+ power?(priority: 50)
+ end
+
+ def power?(priority : Int32 = 0, **options)
+ do_send(:get, :power_status, **options, priority: priority).get
+ !!self[:power].try(&.as_bool)
+ end
+
+ enum Input
+ HDMI = 0x0003 # same as InputB
+ InputA = 0x0002
+ InputB = 0x0003
+ InputC = 0x0004
+ InputD = 0x0005
+ USB = 0x0006 # USB type B
+ Network = 0x0007 # network
+
+ def to_bytes : Bytes
+ Bytes[self.value >> 8, self.value & 0xFF]
+ end
+
+ def self.from_bytes(b : Bytes)
+ Input.from_value((b[0].to_u16 << 8) + b[1])
+ end
+ end
+
+ include PlaceOS::Driver::Interface::InputSelection(Input)
+
+ def switch_to(input : Input)
+ do_send(:set, :input, input.to_bytes) # , delay_on_receive: 500.milliseconds)
+ logger.debug { "requested to switch to: #{input}" }
+
+ input?
+ end
+
+ def input?
+ do_send(:get, :input, priority: 0)
+ end
+
+ def lamp_time?
+ do_send(:get, :lamp_timer, priority: 0)
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ do_send(:set, :mute, Bytes[0, state ? 1 : 0]) # , delay_on_receive: 500)
+ mute?
+ end
+
+ def mute?
+ do_send(:get, :mute, priority: 0)
+ end
+
+ METHODS = [:contrast, :brightness, :color, :hue, :sharpness]
+
+ {% for name in METHODS %}
+ @[Security(Level::Administrator)]
+ def {{name.id}}?
+ do_send(:get, {{name}}, priority: 0)
+ end
+ {% end %}
+
+ {% for name in METHODS %}
+ @[Security(Level::Administrator)]
+ def {{name.id}}(value : UInt8)
+ do_send(:set, {{name}}, Bytes[0, value.clamp(0, 100)], priority: 0)
+ end
+ {% end %}
+
+ enum Command
+ PowerOn = 0x172E
+ PowerOff = 0x172F
+ Input = 0x0001
+ Mute = 0x0030
+ ErrorStatus = 0x0101
+ PowerStatus = 0x0102
+ Contrast = 0x0010
+ Brightness = 0x0011
+ Color = 0x0012
+ Hue = 0x0013
+ Sharpness = 0x0014
+ LampTimer = 0x0113
+
+ def to_bytes : Bytes
+ Bytes[self.value >> 8, self.value & 0xFF]
+ end
+
+ def self.from_bytes(b : Bytes)
+ Command.from_value((b[0].to_u16 << 8) + b[1])
+ end
+ end
+
+ enum CommandType : UInt8
+ Set = 0
+ Get
+ end
+
+ PJTALK_HEADER = Bytes[0x02, 0x0a]
+
+ protected def do_send(cmd_type : CommandType, command : Command, param : Bytes? = nil, name : String | Symbol? = nil, **options)
+ io = IO::Memory.new(14)
+ io.write PJTALK_HEADER
+ io << @community
+ io.write_byte(cmd_type.value)
+ io.write(command.to_bytes)
+
+ if param
+ io.write_byte param.size.to_u8
+ io.write param
+ else
+ io.write_byte 0_u8
+ end
+
+ send(io.to_slice, **options, name: name || (param ? "#{command}_req" : command))
+ end
+
+ def do_poll
+ if power?
+ input?
+ mute?
+ do_send(:get, :error_status, priority: 0)
+ lamp_time?
+ end
+ end
+
+ enum ResponseStatus : UInt8
+ NoGood = 0
+ Okay
+ end
+
+ def received(data, task)
+ logger.debug { "sony proj sent: 0x#{data.hexstring}" }
+
+ response_status = ResponseStatus.from_value data[6]
+ pjt_command = Command.from_bytes data[7..8]
+ pjt_length = data[9]
+ pjt_data = pjt_length > 0 ? data[10..-1] : Bytes.new(0)
+
+ # check for error response
+ if response_status.no_good?
+ category = ERROR_CATEGORY[pjt_data[0]]? || :unknown
+ message = ERRORS[category][pjt_data[1]]? || "unknown: category #{pjt_data[1].to_s(16)}, reason #{pjt_data[1].to_s(16)}"
+
+ self[:last_error] = "#{category}: #{message}"
+ logger.debug { "Command #{pjt_command} failed with #{category}: #{message}" }
+ return task.try &.abort
+ end
+
+ # process a successful response
+ case pjt_command
+ when .power_on?
+ self[:power] = true
+ when .power_off?
+ self[:power] = false
+ when .lamp_timer?
+ # Two bytes converted to a 16bit integer
+ # we use negative indexes as can be a 32bit response (only 16bits needed)
+ self[:lamp_usage] = (pjt_data[-2].to_u16 << 8) + pjt_data[-1]
+ when .power_status?
+ case pjt_data[-1]
+ when 0, 8
+ self[:warming] = self[:cooling] = self[:power] = false
+ when 1, 2
+ self[:cooling] = false
+ self[:warming] = self[:power] = true
+ when 3
+ self[:power] = true
+ self[:warming] = self[:cooling] = false
+ when 4, 5, 6, 7
+ self[:cooling] = true
+ self[:warming] = self[:power] = false
+ end
+ schedule.in(5.seconds) { power? } if self[:warming] || self[:cooling]
+ when .mute?
+ self[:mute] = pjt_data[-1] == 1
+ when .input?
+ self[:input] = Input.from_bytes(pjt_data)
+ when .contrast?, .brightness?, color?, .hue?, .sharpness?
+ self[pjt_command.to_s.downcase] = pjt_data[-1]
+ end
+
+ task.try &.success
+ end
+
+ ERROR_CATEGORY = {
+ 0x01_u8 => :item_error,
+ 0x02_u8 => :community_error,
+ 0x10_u8 => :request_error,
+ 0x20_u8 => :network_error,
+ 0xF0_u8 => :comms_error,
+ 0xF1_u8 => :ram_error,
+ }
+
+ ERRORS = {
+ item_error: {
+ 0x01_u8 => "Invalid Item",
+ 0x02_u8 => "Invalid Item Request",
+ 0x03_u8 => "Invalid Length",
+ 0x04_u8 => "Invalid Data",
+ 0x11_u8 => "Short Data",
+ 0x80_u8 => "Not Applicable Item",
+ },
+ community_error: {
+ 0x01_u8 => "Different Community",
+ },
+ request_error: {
+ 0x01_u8 => "Invalid Version",
+ 0x02_u8 => "Invalid Category",
+ 0x03_u8 => "Invalid Request",
+ 0x11_u8 => "Short Header",
+ 0x12_u8 => "Short Community",
+ 0x13_u8 => "Short Command",
+ },
+ network_error: {
+ 0x01_u8 => "Timeout",
+ },
+ comms_error: {
+ 0x01_u8 => "Timeout",
+ 0x10_u8 => "Check Sum Error",
+ 0x20_u8 => "Framing Error",
+ 0x30_u8 => "Parity Error",
+ 0x40_u8 => "Over Run Error",
+ 0x50_u8 => "Other Comm Error",
+ 0xF0_u8 => "Unknown Response",
+ },
+ ram_error: {
+ 0x10_u8 => "Read Error",
+ 0x20_u8 => "Write Error",
+ },
+ unknown: {} of UInt8 => String,
+ }
+end
diff --git a/drivers/sony/projector/pj_talk_spec.cr b/drivers/sony/projector/pj_talk_spec.cr
new file mode 100644
index 00000000000..781efc141ac
--- /dev/null
+++ b/drivers/sony/projector/pj_talk_spec.cr
@@ -0,0 +1,15 @@
+DriverSpecs.mock_driver "Sony::Projector::PjTalk" do
+ resp = exec(:power?)
+ should_send("020a534f4e5901010200".hexbytes)
+ responds("020a534f4e590101020100".hexbytes)
+ resp.get.should eq false
+ status[:power].should eq(false)
+
+ exec(:power, true)
+ should_send("020a534f4e5900172E00".hexbytes)
+ responds("020a534f4e5901172E00".hexbytes)
+
+ should_send("020a534f4e5901010200".hexbytes)
+ responds("020a534f4e590101020103".hexbytes)
+ status[:power].should eq(true)
+end
diff --git a/drivers/sony/projector/serial_control.cr b/drivers/sony/projector/serial_control.cr
new file mode 100644
index 00000000000..411fec33dd3
--- /dev/null
+++ b/drivers/sony/projector/serial_control.cr
@@ -0,0 +1,228 @@
+require "placeos-driver/interface/powerable"
+require "placeos-driver/interface/muteable"
+require "placeos-driver/interface/switchable"
+
+# Documentation: https://aca.im/driver_docs/Sony/Sony_Q004_R1_protocol.pdf
+
+class Sony::Projector::SerialControl < PlaceOS::Driver
+ include Interface::Powerable
+ include Interface::Muteable
+
+ descriptive_name "Sony Projector (RS232 Control)"
+ generic_name :Display
+
+ INDICATOR = 0xA9_u8
+ DELIMITER = 0x9A_u8
+
+ def on_load
+ transport.tokenizer = Tokenizer.new(Bytes[DELIMITER])
+ end
+
+ def connected
+ schedule.every(60.seconds) { do_poll }
+ end
+
+ def disconnected
+ schedule.clear
+ end
+
+ def power(state : Bool)
+ if state
+ # Need to send twice in case of deep sleep
+ logger.debug { "requested to power on" }
+ do_send(Type::Set, Command::PowerOn, name: :power)
+ do_send(Type::Set, Command::PowerOn, name: :power, delay: 3.seconds)
+ else
+ logger.debug { "requested to power off" }
+ do_send(Type::Set, Command::PowerOff, name: :power, delay: 3.seconds)
+ end
+
+ # Request status update
+ power?(priority: 50)
+ end
+
+ def power?(priority : Int32 = 0, **options)
+ do_send(Type::Get, Command::PowerStatus, **options, priority: priority).get
+ !!self[:power].try(&.as_bool)
+ end
+
+ enum Input
+ HDMI = 0x0003 # same as InputB
+ InputA = 0x0002
+ InputB = 0x0003
+ InputC = 0x0004
+ InputD = 0x0005
+ USB = 0x0006 # USB type B
+ Network = 0x0007 # network
+
+ def to_bytes : Bytes
+ Bytes[self.value >> 8, self.value & 0xFF]
+ end
+
+ def self.from_bytes(b : Bytes)
+ Input.from_value((b[0].to_u16 << 8) + b[1])
+ end
+ end
+
+ include PlaceOS::Driver::Interface::InputSelection(Input)
+
+ def switch_to(input : Input)
+ do_send(Type::Set, Command::Input, input.to_bytes) # , delay_on_receive: 500.milliseconds)
+ logger.debug { "requested to switch to: #{input}" }
+
+ input?
+ end
+
+ def input?
+ do_send(Type::Get, Command::Input, priority: 0)
+ end
+
+ def lamp_time?
+ do_send(Type::Get, Command::LampTimer, priority: 0)
+ end
+
+ def mute(
+ state : Bool = true,
+ index : Int32 | String = 0,
+ layer : MuteLayer = MuteLayer::AudioVideo
+ )
+ do_send(Type::Set, Command::Mute, Bytes[0, state ? 1 : 0]) # , delay_on_receive: 500)
+ mute?
+ end
+
+ def mute?
+ do_send(Type::Get, Command::Mute, priority: 0)
+ end
+
+ METHODS = ["Contrast", "Brightness", "Color", "Hue", "Sharpness"]
+
+ {% for name in METHODS %}
+ @[Security(Level::Administrator)]
+ def {{name.id.downcase}}?
+ do_send(Type::Get, Command::{{name.id}}, priority: 0)
+ end
+ {% end %}
+
+ {% for name in METHODS %}
+ @[Security(Level::Administrator)]
+ def {{name.id.downcase}}(value : UInt8)
+ do_send(Type::Set, Command::{{name.id}}, Bytes[0, value.clamp(0, 100)], priority: 0)
+ end
+ {% end %}
+
+ ERRORS = {
+ 0x00 => "No Error",
+ 0x01 => "Lamp Error",
+ 0x02 => "Fan Error",
+ 0x04 => "Cover Error",
+ 0x08 => "Temperature Error",
+ 0x10 => "D5V Error",
+ 0x20 => "Power Error",
+ 0x40 => "Warning Error",
+ 0x80 => "NVM Data ERROR",
+ }
+
+ private def do_poll
+ if power?(priority: 0)
+ input?
+ mute?
+ do_send(Type::Get, Command::ErrorStatus, priority: 0)
+ lamp_time?
+ end
+ end
+
+ enum Command
+ PowerOn = 0x172E
+ PowerOff = 0x172F
+ Input = 0x0001
+ Mute = 0x0030
+ ErrorStatus = 0x0101
+ PowerStatus = 0x0102
+ Contrast = 0x0010
+ Brightness = 0x0011
+ Color = 0x0012
+ Hue = 0x0013
+ Sharpness = 0x0014
+ LampTimer = 0x0113
+
+ def to_bytes : Bytes
+ Bytes[self.value >> 8, self.value & 0xFF]
+ end
+
+ def self.from_bytes(b : Bytes)
+ Command.from_value((b[0].to_u16 << 8) + b[1])
+ end
+ end
+
+ enum Type : UInt8
+ Set
+ Get
+ end
+
+ private def do_send(type : Type, command : Command, param : Bytes = Bytes.new(2), **options)
+ # indicator: 1, command: 2, type: 1, param: 2, checksum: 1, delimiter: 1
+ data = Bytes.new(8).tap do |bytes|
+ bytes[0] = INDICATOR
+ command.to_bytes.each_with_index(1) { |b, i| bytes[i] = b } # bytes[1..2]
+ bytes[3] = type.value
+ param.each_with_index(4) { |b, i| bytes[i] = b } # bytes[4..5]
+ bytes[7] = DELIMITER
+ end
+ data[6] = data[1..5].reduce { |a, b| a |= b } # checksum
+
+ send(data, **options)
+ end
+
+ def received(data, task)
+ logger.debug { "sony proj sent: 0x#{data.hexstring}" }
+
+ cmd = data[1..2]
+ type = data[3]
+ resp = data[4..5]
+
+ checksum = data[1..5].reduce { |a, b| a |= b }
+ return task.try &.abort("Checksum should be 0x#{checksum.to_s(base: 16, upcase: true)}") unless data[6] == checksum
+
+ # Check if an ACK/NAK
+ if type == 0x03
+ if cmd == Bytes[0, 0]
+ return task.try &.success
+ else # Command failed
+ return task.try &.abort("Command failed with 0x#{cmd.join(&.to_s(base: 16, upcase: true))}")
+ end
+ else
+ case command = Command.from_bytes(cmd)
+ when .power_on?
+ self[:power] = true
+ when .power_off?
+ self[:power] = false
+ when .lamp_timer?
+ # Two bytes converted to a 16bit integer
+ self[:lamp_usage] = (resp[-2].to_u16 << 8) + resp[-1]
+ when .power_status?
+ case resp[-1]
+ when 0, 8
+ self[:warming] = self[:cooling] = self[:power] = false
+ when 1, 2
+ self[:cooling] = false
+ self[:warming] = self[:power] = true
+ when 3
+ self[:power] = true
+ self[:warming] = self[:cooling] = false
+ when 4, 5, 6, 7
+ self[:cooling] = true
+ self[:warming] = self[:power] = false
+ end
+ schedule.in(5.seconds) { power? } if self[:warming] || self[:cooling]
+ when .mute?
+ self[:mute] = resp[-1] == 1
+ when .input?
+ self[:input] = Input.from_bytes(resp)
+ when .contrast?, .brightness?, color?, .hue?, .sharpness?
+ self[command.to_s.downcase] = resp[-1]
+ end
+ end
+
+ task.try &.success
+ end
+end
diff --git a/drivers/sony/projector/serial_control_spec.cr b/drivers/sony/projector/serial_control_spec.cr
new file mode 100644
index 00000000000..446a68442a9
--- /dev/null
+++ b/drivers/sony/projector/serial_control_spec.cr
@@ -0,0 +1,46 @@
+DriverSpecs.mock_driver "Sony::Projector::SerialControl" do
+ exec(:power, true)
+ should_send("\xA9\x17\x2E\x00\x00\x00\x3F\x9A")
+ responds("\xA9\x00\x00\x03\x00\x00\x03\x9A")
+ should_send("\xA9\x17\x2E\x00\x00\x00\x3F\x9A")
+ responds("\xA9\x00\x00\x03\x00\x00\x03\x9A")
+ sleep 3
+ # power?
+ should_send("\xA9\x01\x02\x01\x00\x00\x03\x9A")
+ responds("\xA9\x01\x02\x02\x00\x03\x03\x9A")
+ status[:cooling].should eq(false)
+ status[:warming].should eq(false)
+ status[:power].should eq(true)
+
+ exec(:switch_to, "hdmi")
+ should_send("\xA9\x00\x01\x00\x00\x03\x03\x9A")
+ responds("\xA9\x00\x00\x03\x00\x00\x03\x9A")
+ # input?
+ should_send("\xA9\x00\x01\x01\x00\x00\x01\x9A")
+ responds("\xA9\x00\x01\x02\x00\x03\x03\x9A")
+ status[:input].should eq("HDMI")
+
+ exec(:mute)
+ should_send("\xA9\x00\x30\x00\x00\x01\x31\x9A")
+ responds("\xA9\x00\x00\x03\x00\x00\x03\x9A")
+ # mute?
+ should_send("\xA9\x00\x30\x01\x00\x00\x31\x9A")
+ responds("\xA9\x00\x30\x02\x00\x01\x33\x9A")
+ status[:mute].should eq(true)
+
+ exec(:lamp_time?)
+ should_send("\xA9\x01\x13\x01\x00\x00\x13\x9A")
+ responds("\xA9\x01\x13\x02\x03\xE8\xFB\x9A")
+ status[:lamp_usage].should eq(1000)
+
+ exec(:power, false)
+ should_send("\xA9\x17\x2F\x00\x00\x00\x3F\x9A")
+ responds("\xA9\x00\x00\x03\x00\x00\x03\x9A")
+ sleep 3
+ # power?
+ should_send("\xA9\x01\x02\x01\x00\x00\x03\x9A")
+ responds("\xA9\x01\x02\x02\x00\x04\x07\x9A")
+ status[:cooling].should eq(true)
+ status[:warming].should eq(false)
+ status[:power].should eq(false)
+end
diff --git a/drivers/steinel/hpd2.cr b/drivers/steinel/hpd2.cr
new file mode 100644
index 00000000000..9ffd72e3bcb
--- /dev/null
+++ b/drivers/steinel/hpd2.cr
@@ -0,0 +1,126 @@
+module Steinel; end
+
+class Steinel::HPD2 < PlaceOS::Driver
+ # Discovery Information
+ generic_name :PeopleCounter
+ descriptive_name "Steinel HPD-2"
+
+ # Local network
+ uri_base "https://192.168.0.20"
+
+ default_settings({
+ basic_auth: {
+ username: "admin",
+ password: "steinel",
+ },
+ })
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ schedule.every(5.seconds) { get_status }
+ end
+
+ def get_status
+ response = get("/api/sensorstatus.php")
+
+ logger.debug { "received #{response.body}" }
+
+ if response.success?
+ SensorStatus.from_json(response.body.not_nil!)
+ else
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+
+ class SensorStatus
+ include JSON::Serializable
+
+ @[JSON::Field(key: "AppVersion")]
+ property app_version : String
+
+ @[JSON::Field(key: "FpgaVersion")]
+ property fpga_version : String
+
+ @[JSON::Field(key: "KnxSapNumber")]
+ property knx_sap_number : String
+
+ @[JSON::Field(key: "KnxVersion")]
+ property knx_version : String
+
+ @[JSON::Field(key: "KnxAddr")]
+ property knx_address : String
+
+ @[JSON::Field(key: "GitRevision")]
+ property git_revision : String
+
+ @[JSON::Field(key: "ModelName")]
+ property model_name : String
+
+ @[JSON::Field(key: "FrameProcessingTimeMs")]
+ property frame_processing_time_ms : Int32
+
+ @[JSON::Field(key: "AverageFps5")]
+ property average_fps5 : Float64
+
+ @[JSON::Field(key: "AverageFps50")]
+ property average_fps50 : Float64
+
+ @[JSON::Field(key: "RunningTimeHHMMSS")]
+ property running_time : String
+
+ @[JSON::Field(key: "UptimeHHMMSS")]
+ property uptime : String
+
+ @[JSON::Field(key: "IrLedOn")]
+ property ir_led_on : Int32
+
+ @[JSON::Field(key: "DetectedPersons")]
+ property detected_persons : Int32
+
+ @[JSON::Field(key: "PersonPresence")]
+ property person_presence : Int32
+
+ @[JSON::Field(key: "DetectedPersonsZone")]
+ property detected_persons_zone : Array(Int32)
+
+ @[JSON::Field(key: "PersonPresenceZone")]
+ property person_presence_zone : Array(Int32)
+
+ @[JSON::Field(key: "DetectionZonesPresent")]
+ property detection_zones_present : Int32
+
+ @[JSON::Field(key: "GlobalIlluminanceLux")]
+ property global_illuminance_lux : Float64
+
+ @[JSON::Field(key: "LuxZone")]
+ property lux_zone : Array(Float64)
+
+ @[JSON::Field(key: "GlobalLightValue")]
+ property global_light_value : Int32
+
+ @[JSON::Field(key: "ArmsensorCpuUsage")]
+ property arm_sensor_cpu_usage : String
+
+ @[JSON::Field(key: "WebServerCpuUsage")]
+ property web_server_cpu_usage : String
+
+ @[JSON::Field(key: "Temperature")]
+ property temperature : String
+
+ @[JSON::Field(key: "Humidity")]
+ property humidity : String
+
+ @[JSON::Field(key: "KnxDetected")]
+ property knx_detected : String
+
+ @[JSON::Field(key: "KnxProgramMode")]
+ property knx_program_mode : String
+
+ @[JSON::Field(key: "KnxLedState")]
+ property knx_led_state : String
+ property final : String
+ end
+end
diff --git a/drivers/steinel/hpd2_spec.cr b/drivers/steinel/hpd2_spec.cr
new file mode 100644
index 00000000000..f9b1a7adc17
--- /dev/null
+++ b/drivers/steinel/hpd2_spec.cr
@@ -0,0 +1,27 @@
+DriverSpecs.mock_driver "Xovis::SensorAPI" do
+ # Send the request
+ retval = exec(:get_status)
+ data = %({"AppVersion": "3.2.3", "FpgaVersion": "v300", "KnxSapNumber": "0", "KnxVersion": "0", "KnxAddr":
+ "", "GitRevision": "d45734c2", "ModelName": "15_2xroute_fix26", "FrameProcessingTimeMs": 1179,
+ "AverageFps5": 0.850314, "AverageFps50": 0.855873, "RunningTimeHHMMSS": "672:55:58",
+ "UptimeHHMMSS": "672:56:35", "IrLedOn": 0, "DetectedPersons": 0, "PersonPresence": 0,
+ "DetectedPersonsZone": [0, 0, 0, 0, 0], "PersonPresenceZone": [0, 0, 0, 0, 0],
+ "DetectionZonesPresent": 0, "GlobalIlluminanceLux": 39.0, "LuxZone": [0.0, 0.0, 0.0, 0.0, 0.0],
+ "GlobalLightValue": 72, "ArmsensorCpuUsage": "20", "WebServerCpuUsage": "2", "Temperature":
+ "27.745661", "Humidity": "25.286158", "KnxDetected": "0", "KnxProgramMode": "0", "KnxLedState":
+ "0", "final": "OK" })
+
+ # We should request a new token from Floorsense
+ expect_http_request do |request, response|
+ if request.headers["Authorization"]? == "Basic YWRtaW46c3RlaW5lbA=="
+ response.status_code = 200
+ response.output.puts data
+ else
+ puts request.headers.inspect
+ response.status_code = 401
+ end
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq(JSON.parse(data))
+end
diff --git a/drivers/vergesense/location_service.cr b/drivers/vergesense/location_service.cr
new file mode 100644
index 00000000000..60614ad96c7
--- /dev/null
+++ b/drivers/vergesense/location_service.cr
@@ -0,0 +1,117 @@
+module Vergesense; end
+
+require "json"
+require "oauth2"
+require "placeos-driver/interface/locatable"
+require "./models"
+
+class Vergesense::LocationService < PlaceOS::Driver
+ include Interface::Locatable
+
+ descriptive_name "Vergesense Location Service"
+ generic_name :VergesenseLocationService
+ description %(collects desk booking data from the staff API and overlays Vergesense data for visualising on a map)
+
+ accessor area_manager : AreaManagement_1
+ accessor vergesense : Vergesense_1
+
+ default_settings({
+ floor_mappings: {
+ "vergesense_building_id-floor_id": {
+ building_id: "zone-building",
+ level_id: "zone-level",
+ name: "friendly name for documentation",
+ },
+ },
+ })
+
+ @floor_mappings : Hash(String, NamedTuple(building_id: String?, level_id: String)) = {} of String => NamedTuple(building_id: String?, level_id: String)
+ @zone_filter : Array(String) = [] of String
+ @building_mappings : Hash(String, String?) = {} of String => String?
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @floor_mappings = setting(Hash(String, NamedTuple(building_id: String?, level_id: String)), :floor_mappings)
+ @zone_filter = @floor_mappings.values.map do |z|
+ level = z[:level_id]
+ @building_mappings[level] = z[:building_id]
+ level
+ end
+
+ bind_floor_status
+ end
+
+ # ===================================
+ # Bindings into Vergesense data
+ # ===================================
+ protected def bind_floor_status
+ subscriptions.clear
+
+ @floor_mappings.each do |floor_id, details|
+ zone_id = details[:level_id]
+ vergesense.subscribe(floor_id) do |_sub, payload|
+ level_state_change(zone_id, Floor.from_json(payload))
+ end
+ end
+ end
+
+ # Zone_id => Floor
+ @occupancy_mappings : Hash(String, Floor) = {} of String => Floor
+
+ protected def level_state_change(zone_id, floor)
+ @occupancy_mappings[zone_id] = floor
+ area_manager.update_available({zone_id})
+ rescue error
+ logger.error(exception: error) { "error updating level #{zone_id} space changes" }
+ end
+
+ # ===================================
+ # Locatable Interface functions
+ # ===================================
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "sensor incapable of locating #{email} or #{username}" }
+ [] of Nil
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ logger.debug { "sensor incapable of tracking #{email} or #{username}" }
+ [] of String
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ logger.debug { "sensor incapable of tracking #{mac_address}" }
+ nil
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching locatable in zone #{zone_id}" }
+ return [] of Nil unless @zone_filter.includes?(zone_id)
+
+ floor = @occupancy_mappings[zone_id]?
+ return [] of Nil unless floor
+
+ floor.spaces.compact_map do |space|
+ loc_type = space.space_type == "desk" ? "desk" : "area"
+ next if location.presence && location != loc_type
+
+ people_count = space.people.try(&.count)
+
+ if people_count && people_count > 0
+ {
+ location: loc_type,
+ at_location: people_count,
+ map_id: space.name,
+ level: zone_id,
+ building: @building_mappings[zone_id]?,
+ capacity: space.capacity,
+
+ vergesense_space_id: space.space_ref_id,
+ vergesense_space_type: space.space_type,
+ }
+ end
+ end
+ end
+end
diff --git a/drivers/vergesense/location_service_spec.cr b/drivers/vergesense/location_service_spec.cr
new file mode 100644
index 00000000000..71ca9d59af8
--- /dev/null
+++ b/drivers/vergesense/location_service_spec.cr
@@ -0,0 +1,64 @@
+DriverSpecs.mock_driver "Vergesense::LocationService" do
+ system({
+ Vergesense: {Vergesense},
+ AreaManagement: {AreaManagement},
+ })
+
+ resp = exec(:device_locations, "zone-level").get
+ resp.should eq([
+ {"location" => "area", "at_location" => 21, "map_id" => "Conference Room 0721", "level" => "zone-level", "building" => "zone-building", "capacity" => 30, "vergesense_space_id" => "CR_0721", "vergesense_space_type" => "conference_room"},
+ {"location" => "desk", "at_location" => 1, "map_id" => "desk-1234", "level" => "zone-level", "building" => "zone-building", "capacity" => 1, "vergesense_space_id" => "CR_0722", "vergesense_space_type" => "desk"},
+ ])
+end
+
+class Vergesense < DriverSpecs::MockDriver
+ def on_load
+ self["vergesense_building_id-floor_id"] = {
+ "floor_ref_id" => "floor_id",
+ "name" => "Floor 1",
+ "capacity" => 84,
+ "max_capacity" => 60,
+ "spaces" => [
+ {
+ "building_ref_id" => "vergesense_building_id",
+ "floor_ref_id" => "floor_id",
+ "space_ref_id" => "CR_0721",
+ "space_type" => "conference_room",
+ "name" => "Conference Room 0721",
+ "capacity" => 30,
+ "max_capacity" => 32,
+ "geometry" => {"type" => "Polygon", "coordinates" => [[[93.850772, 44.676952], [93.850739, 44.676929], [93.850718, 44.67695], [93.850751, 44.676973], [93.850772, 44.676952], [93.850772, 44.676952]]]},
+ "people" => {
+ "count" => 21,
+ "coordinates" => [[[2.2673, 4.3891], [6.2573, 1.5303]]],
+ },
+ "timestamp" => "2019-08-21T21:10:25Z",
+ "motion_detected" => true,
+ },
+ {
+ "building_ref_id" => "vergesense_building_id",
+ "floor_ref_id" => "floor_id",
+ "space_ref_id" => "CR_0722",
+ "space_type" => "desk",
+ "name" => "desk-1234",
+ "capacity" => 1,
+ "max_capacity" => 1,
+ "geometry" => {"type" => "Polygon", "coordinates" => [[[93.850772, 44.676952], [93.850739, 44.676929], [93.850718, 44.67695], [93.850751, 44.676973], [93.850772, 44.676952], [93.850772, 44.676952]]]},
+ "people" => {
+ "count" => 1,
+ "coordinates" => [[[2.2673, 4.3891], [6.2573, 1.5303]]],
+ },
+ "timestamp" => "2019-08-21T21:10:25Z",
+ "motion_detected" => true,
+ },
+ ],
+ }
+ end
+end
+
+class AreaManagement < DriverSpecs::MockDriver
+ def update_available(zones : Array(String))
+ logger.info { "requested update to #{zones}" }
+ nil
+ end
+end
diff --git a/drivers/vergesense/models.cr b/drivers/vergesense/models.cr
new file mode 100644
index 00000000000..36f72e4f68b
--- /dev/null
+++ b/drivers/vergesense/models.cr
@@ -0,0 +1,63 @@
+require "json"
+
+# Vergesense Data Models
+module Vergesense
+ struct Building
+ include JSON::Serializable
+
+ property name : String
+ property building_ref_id : String
+ property address : String?
+ end
+
+ struct BuildingWithFloors
+ include JSON::Serializable
+
+ property building_ref_id : String
+ property floors : Array(Floor)
+ end
+
+ struct Floor
+ include JSON::Serializable
+
+ property floor_ref_id : String
+ property name : String
+ property capacity : UInt32?
+ property max_capacity : UInt32?
+ property spaces : Array(Space)
+ end
+
+ class Space
+ include JSON::Serializable
+
+ property building_ref_id : String?
+ property floor_ref_id : String?
+ property space_ref_id : String
+ property space_type : String?
+ property name : String?
+ property capacity : UInt32?
+ property max_capacity : UInt32?
+ property geometry : Geometry?
+ property people : People?
+ property timestamp : String?
+ property motion_detected : Bool?
+
+ def floor_key
+ "#{building_ref_id}-#{floor_ref_id}".strip
+ end
+ end
+
+ struct Geometry
+ include JSON::Serializable
+
+ property type : String
+ property coordinates : Array(Array(Array(Float64)))
+ end
+
+ struct People
+ include JSON::Serializable
+
+ property count : UInt32?
+ property coordinates : Array(Array(Array(Float64)))?
+ end
+end
diff --git a/drivers/vergesense/vergesense_api.cr b/drivers/vergesense/vergesense_api.cr
new file mode 100644
index 00000000000..1843d9acd2b
--- /dev/null
+++ b/drivers/vergesense/vergesense_api.cr
@@ -0,0 +1,147 @@
+module Vergesense; end
+
+require "./models"
+
+class Vergesense::VergesenseAPI < PlaceOS::Driver
+ # Discovery Information
+ descriptive_name "Vergesense API"
+ generic_name :Vergesense
+ uri_base "https://api.vergesense.com"
+ description "for more information visit: https://vergesense.readme.io/"
+
+ default_settings({
+ vergesense_api_key: "VS-API-KEY",
+ })
+
+ @api_key : String = ""
+
+ @buildings : Array(Building) = [] of Building
+ @floors : Hash(String, Floor) = {} of String => Floor
+
+ @debug_payload : Bool = false
+ @poll_every : Time::Span? = nil
+ @sync_lock : Mutex = Mutex.new
+
+ def on_load
+ on_update
+ schedule.in(200.milliseconds) { init_sync }
+ end
+
+ def on_update
+ @api_key = setting(String, :vergesense_api_key)
+ @debug_payload = setting?(Bool, :debug_payload) || false
+
+ @poll_every = setting?(Int32, :poll_every).try &.seconds
+
+ schedule.clear
+ if poll_time = @poll_every
+ schedule.every(poll_time) { init_sync }
+ end
+ end
+
+ # Performs initial sync by loading buildings / floors / spaces
+ def init_sync
+ @sync_lock.synchronize do
+ init_buildings
+
+ if @buildings
+ init_floors
+ init_spaces
+ init_floors_status
+ end
+ end
+ rescue e
+ logger.error { "failed to perform vergesense API sync\n#{e.inspect_with_backtrace}" }
+ end
+
+ EMPTY_HEADERS = {} of String => String
+ SUCCESS_RESPONSE = {HTTP::Status::OK, EMPTY_HEADERS, nil}
+
+ # Webhook endpoint for space_report API, expects version 2
+ def space_report_api(method : String, headers : Hash(String, Array(String)), body : String)
+ logger.debug { "space_report API received: #{method},\nheaders #{headers},\nbody size #{body.size}" }
+ logger.debug { body } if @debug_payload
+
+ # Parse the data posted
+ begin
+ remote_space = Space.from_json(body)
+ logger.debug { "parsed vergesense payload" }
+
+ update_floor_space(remote_space)
+ update_single_floor_status(remote_space.floor_key, @floors[remote_space.floor_key]?)
+ rescue e
+ logger.error { "failed to parse vergesense space_report API payload\n#{e.inspect_with_backtrace}" }
+ logger.debug { "failed payload body was\n#{body}" }
+ end
+
+ # Return a 200 response
+ SUCCESS_RESPONSE
+ end
+
+ private def init_buildings
+ @buildings = Array(Building).from_json(get_request("/buildings"))
+ end
+
+ private def init_floors
+ @buildings.not_nil!.each do |building|
+ building_with_floors = BuildingWithFloors.from_json(get_request("/buildings/#{building.building_ref_id}"))
+ if building_with_floors
+ building_with_floors.floors.each do |floor|
+ floor_key = "#{building.building_ref_id}-#{floor.floor_ref_id}".strip
+ @floors[floor_key] = floor
+ end
+ end
+ end
+ @floors
+ end
+
+ private def init_spaces
+ spaces = Array(Space).from_json(get_request("/spaces"))
+ spaces.each do |remote_space|
+ update_floor_space(remote_space)
+ end
+
+ spaces
+ end
+
+ private def init_floors_status
+ @floors.each do |floor_key, floor|
+ update_single_floor_status(floor_key, floor)
+ end
+ end
+
+ private def update_single_floor_status(floor_key, floor)
+ if floor_key && floor
+ self[floor_key] = floor.not_nil!
+ end
+ end
+
+ # Finds a space on a given floor and updates it in place.
+ private def update_floor_space(remote_space)
+ floor = @floors[remote_space.floor_key]?
+ if floor
+ floor_space = floor.spaces.find { |space| space.space_ref_id == remote_space.space_ref_id }
+ if floor_space
+ floor_space.building_ref_id = remote_space.building_ref_id
+ floor_space.floor_ref_id = remote_space.floor_ref_id
+ floor_space.people = remote_space.people
+ floor_space.motion_detected = remote_space.motion_detected
+ floor_space.timestamp = remote_space.timestamp
+ end
+ end
+ end
+
+ private def get_request(path)
+ response = get(path,
+ headers: {
+ "vs-api-key" => @api_key,
+ }
+ )
+
+ if response.success?
+ response.body
+ else
+ raise "unexpected response #{response.status_code}\n#{response.body}"
+ end
+ end
+end
diff --git a/drivers/vergesense/vergesense_api_spec.cr b/drivers/vergesense/vergesense_api_spec.cr
new file mode 100644
index 00000000000..5c2fa9e6b2c
--- /dev/null
+++ b/drivers/vergesense/vergesense_api_spec.cr
@@ -0,0 +1,200 @@
+DriverSpecs.mock_driver "Vergesense::VergesenseAPI" do
+ expect_http_request do |request, response|
+ case request.path
+ when "/buildings"
+ response.status_code = 200
+ response << %([{
+ "name": "HQ 1",
+ "building_ref_id": "HQ1",
+ "address": null
+ }])
+ end
+ end
+
+ puts "SENT BUILDINGS"
+
+ expect_http_request do |request, response|
+ case request.path
+ when "/buildings/HQ1"
+ response.status_code = 200
+ response << %({
+ "building_ref_id": "HQ1",
+ "capacity": 84,
+ "minimum_social_distance": 2.0,
+ "floors": [
+ {
+ "name": "Floor 1",
+ "floor_ref_id": "FL1",
+ "capacity": 84,
+ "max_capacity": 60,
+ "spaces": [
+ {
+ "name": "Conference Room 0721",
+ "space_ref_id": "CR_0721",
+ "space_type": "conference_room",
+ "capacity": 4,
+ "max_capacity": 3,
+ "sensors": [
+ {
+ "id": "L_000018",
+ "partitions": [
+ {
+ "id": "L_000018/321"
+ }
+ ]
+ }
+ ],
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ [
+ [
+ 93.850772,
+ 44.676952
+ ],
+ [
+ 93.850739,
+ 44.676929
+ ],
+ [
+ 93.850718,
+ 44.67695
+ ],
+ [
+ 93.850751,
+ 44.676973
+ ],
+ [
+ 93.850772,
+ 44.676952
+ ],
+ [
+ 93.850772,
+ 44.676952
+ ]
+ ]
+ ]
+ }
+ }
+ ]
+ }
+ ]
+})
+ end
+ end
+
+ puts "SENT FLOORS"
+
+ expect_http_request do |request, response|
+ case request.path
+ when "/spaces"
+ response.status_code = 200
+ response << %([
+ {
+ "building_ref_id": "HQ1",
+ "floor_ref_id": "FL1",
+ "space_ref_id": "CR_0721",
+ "name": "Conference Room 0721",
+ "space_type": "conference_room",
+ "last_reports": [
+ {
+ "id": "W91-IGI",
+ "person_count": 2,
+ "signs_of_life": null,
+ "motion_detected": null,
+ "timestamp": "2019-07-29T18:42:19Z"
+ }
+ ],
+ "people": {
+ "count": 2,
+ "distances": {
+ "units": "meters",
+ "values": [2.42]
+ }
+ }
+ }
+])
+ end
+ end
+
+ puts "SENT SPACES"
+
+ status["HQ1-FL1"].should eq({
+ "floor_ref_id" => "FL1",
+ "name" => "Floor 1",
+ "capacity" => 84,
+ "max_capacity" => 60,
+ "spaces" => [
+ {
+ "building_ref_id" => "HQ1",
+ "floor_ref_id" => "FL1",
+ "space_ref_id" => "CR_0721",
+ "space_type" => "conference_room",
+ "name" => "Conference Room 0721",
+ "capacity" => 4,
+ "max_capacity" => 3,
+ "geometry" => {"type" => "Polygon", "coordinates" => [[[93.850772, 44.676952], [93.850739, 44.676929], [93.850718, 44.67695], [93.850751, 44.676973], [93.850772, 44.676952], [93.850772, 44.676952]]]},
+ "people" => {"count" => 2},
+ },
+ ],
+ })
+
+ # Testing webhook save
+ webhook_space_report_event = %({
+ "building_ref_id": "HQ1",
+ "floor_ref_id": "FL1",
+ "space_ref_id": "CR_0721",
+ "sensor_ids": ["VS0-123", "VS1-321"],
+ "person_count": 21,
+ "signs_of_life": null,
+ "motion_detected": true,
+ "event_type": "space_report",
+ "timestamp": "2019-08-21T21:10:25Z",
+ "people": {
+ "count": 21,
+ "coordinates": [
+ [
+ [
+ 2.2673,
+ 4.3891
+ ],
+ [
+ 6.2573,
+ 1.5303
+ ]
+ ]
+ ],
+ "distances": {
+ "units": "meters",
+ "values": [1.5]
+ }
+ }
+ })
+
+ exec(:space_report_api, method: "update", headers: {"test" => ["test"]}, body: webhook_space_report_event).get
+
+ status["HQ1-FL1"].should eq({
+ "floor_ref_id" => "FL1",
+ "name" => "Floor 1",
+ "capacity" => 84,
+ "max_capacity" => 60,
+ "spaces" => [
+ {
+ "building_ref_id" => "HQ1",
+ "floor_ref_id" => "FL1",
+ "space_ref_id" => "CR_0721",
+ "space_type" => "conference_room",
+ "name" => "Conference Room 0721",
+ "capacity" => 4,
+ "max_capacity" => 3,
+ "geometry" => {"type" => "Polygon", "coordinates" => [[[93.850772, 44.676952], [93.850739, 44.676929], [93.850718, 44.67695], [93.850751, 44.676973], [93.850772, 44.676952], [93.850772, 44.676952]]]},
+ "people" => {
+ "count" => 21,
+ "coordinates" => [[[2.2673, 4.3891], [6.2573, 1.5303]]],
+ },
+ "timestamp" => "2019-08-21T21:10:25Z",
+ "motion_detected" => true,
+ },
+ ],
+ })
+end
diff --git a/drivers/whispir/messages.cr b/drivers/whispir/messages.cr
new file mode 100644
index 00000000000..24e42b32a7f
--- /dev/null
+++ b/drivers/whispir/messages.cr
@@ -0,0 +1,59 @@
+module Whispir; end
+
+# Documentation: https://whispir.github.io/api/#messages
+require "placeos-driver/interface/sms"
+
+class Whispir::Messages < PlaceOS::Driver
+ include Interface::SMS
+
+ # Discovery Information
+ generic_name :SMS
+ descriptive_name "Whispir messages service"
+ uri_base "https://api.au.whispir.com"
+
+ # For whatever reason, you need both basic auth and an API key
+ default_settings({
+ basic_auth: {
+ username: "username",
+ password: "password",
+ },
+ api_key: "12345",
+ })
+
+ def on_load
+ on_update
+ end
+
+ @api_key : String = ""
+
+ def on_update
+ @api_key = setting(String, :api_key)
+ end
+
+ def send_sms(
+ phone_numbers : String | Array(String),
+ message : String,
+ format : String? = "SMS",
+ source : String? = nil
+ )
+ phone_numbers = [phone_numbers] unless phone_numbers.is_a?(Array)
+
+ response = post("/messages?apikey=#{@api_key}", body: {
+ to: phone_numbers.join(";"),
+ # As far as I can tell, this field is not passed to the recipients
+ subject: "PlaceOS Notification",
+ body: message,
+ }.to_json, headers: {
+ "Content-Type" => "application/vnd.whispir.message-v1+json",
+ "Accept" => "application/vnd.whispir.message-v1+json",
+ "x-api-key" => @api_key,
+ })
+
+ raise "request failed with #{response.status_code}" unless response.status_code == 202
+
+ location = response.headers["Location"]?
+ logger.debug { "message sent: #{location}" }
+
+ location
+ end
+end
diff --git a/drivers/whispir/messages_spec.cr b/drivers/whispir/messages_spec.cr
new file mode 100644
index 00000000000..8d654bc90e3
--- /dev/null
+++ b/drivers/whispir/messages_spec.cr
@@ -0,0 +1,30 @@
+DriverSpecs.mock_driver "Whispir::Messages" do
+ # Send the request
+ retval = exec(:send_sms,
+ phone_numbers: "+61418419954",
+ message: "hello steve"
+ )
+
+ # sms should send a HTTP request
+ expect_http_request do |request, response|
+ headers = request.headers
+ io = request.body
+ if io
+ data = io.gets_to_end
+ request = JSON.parse(data)
+ if request["to"] == "+61418419954" &&
+ headers["x-api-key"]? == "12345" &&
+ headers["Authorization"]? == "Basic #{Base64.strict_encode("username:password")}"
+ response.status_code = 202
+ response.headers["Location"] = "https://api.au.whispir.com/messages/id"
+ else
+ response.status_code = 401
+ end
+ else
+ raise "expected request to include dialing details #{request.inspect}"
+ end
+ end
+
+ # What the sms function should return
+ retval.get.should eq("https://api.au.whispir.com/messages/id")
+end
diff --git a/drivers/xovis/sensor_api.cr b/drivers/xovis/sensor_api.cr
new file mode 100644
index 00000000000..b7679dbb6bb
--- /dev/null
+++ b/drivers/xovis/sensor_api.cr
@@ -0,0 +1,206 @@
+module Xovis; end
+
+require "xml"
+
+class Xovis::SensorAPI < PlaceOS::Driver
+ # Discovery Information
+ generic_name :XovisSensor
+ descriptive_name "Xovis Flow Sensor"
+
+ uri_base "https://192.168.0.1"
+
+ default_settings({
+ basic_auth: {
+ username: "account",
+ password: "password!",
+ },
+ poll_rate: 15,
+ })
+
+ def on_load
+ on_update
+ end
+
+ @poll_rate : Time::Span = 15.seconds
+
+ def on_update
+ @poll_rate = (setting?(Int32, :poll_rate) || 15).seconds
+ schedule.clear
+ schedule.every(@poll_rate) do
+ count_data
+ capacity_data
+ end
+ schedule.every(5.minutes) { device_status }
+ schedule.in(5.seconds) do
+ count_data
+ capacity_data
+ device_status
+ end
+ end
+
+ # Alternative to using basic auth, but here really only for testing with postman
+ @[Security(Level::Support)]
+ def get_token
+ response = get("/api/auth/token", headers: {"Accept" => "text"})
+ raise "issue obtaining token: #{response.status_code}\n#{response.body}" unless response.success?
+ response.body
+ end
+
+ @[Security(Level::Support)]
+ def get_logs
+ response = get("/api/info/log", headers: {"Accept" => "text"})
+ raise "issue obtaining logs: #{response.status_code}\n#{response.body}" unless response.success?
+ response.body
+ end
+
+ @[Security(Level::Support)]
+ def reset_count
+ response = get("/api/count-data/reset", headers: {"Accept" => "text/xml"})
+ check_success(response)
+ true
+ end
+
+ def is_alive?
+ response = get("/api/info/alive", headers: {"Accept" => "text/xml"})
+ check_success(response)
+ true
+ rescue
+ false
+ end
+
+ def count_data
+ response = get("/api/count-data", headers: {"Accept" => "text/xml"})
+ document = check_success(response)
+
+ lines = {} of String => NamedTuple(name: String, id: String, type: String, sensor: String, data: Hash(String, String | Int32 | Float32))
+ lines_xml = document.xpath_nodes("//lines/line")
+
+ self[:lines] = lines_xml.map do |line|
+ attrs = {} of String => String | Hash(String, Int32)
+ counts = {} of String => Int32
+ line.attributes.each { |attr| attrs[attr.name] = attr.content }
+ line.children.each { |child|
+ next if child.name == "text"
+ counts[child.name] = child.text.to_i
+ }
+ attrs["counts"] = counts
+ attrs
+ end
+ end
+
+ def capacity_data
+ response = get("/api/info/persistence", headers: {"Accept" => "text/xml"})
+ document = check_success(response)
+
+ {"line", "zone-occupancy", "zone-in-out"}.each do |count_name|
+ xml_key_name = "//count-#{count_name}-storage"
+ if count_data = document.xpath_nodes(xml_key_name).first?
+ count_type = count_name.split("-", 2)[0]
+ capacity = xpath_text(document, "#{xml_key_name}/capacity", &.to_i)
+
+ self["#{count_name}-counts"] = document.xpath_nodes("#{xml_key_name}/count-#{count_type}s/count-#{count_type}").map do |zone|
+ attrs = {} of String => String | Int32 | Time | Nil
+
+ zone.children.each do |child|
+ content = child.text.strip
+ attrs[child.name] = case child.name
+ when "entry-count"
+ content.to_i
+ when "first-entry", "last-entry"
+ content.empty? ? nil : Time.parse!(content, "%Y-%m-%dT%H:%M:%S%z")
+ when "text"
+ next
+ else
+ content
+ end
+ end
+
+ attrs["capacity"] = capacity
+ attrs
+ end
+ end
+ end
+ true
+ end
+
+ # Combined `/info` and `/info/status`
+ def device_status
+ response = get("/api/info/sensor-status", headers: {"Accept" => "text/xml"})
+ document = check_success(response)
+
+ parse_type_info(document, "version")
+ parse_type_info(document, "temperature")
+
+ parse_text_info(document, "sensor")
+ parse_text_info(document, "illumination")
+ parse_text_info(document, "configuration")
+ parse_text_info(document, "operation")
+
+ true
+ end
+
+ protected def xpath_text(document, path)
+ document.xpath_nodes(path).first?.try(&.text.strip)
+ end
+
+ protected def xpath_text(document, path)
+ if node = document.xpath_nodes(path).first?
+ yield node.text.strip
+ end
+ end
+
+ protected def parse_type_info(document, xpath_key) : Nil
+ ver_data = document.xpath_nodes("//#{xpath_key}s/#{xpath_key}")
+ attrs = {} of String => String
+ ver_data.each do |data|
+ key = data.attributes.find(&.name.==("type")).try &.content
+ next unless key
+ attrs[key] = data.text.strip
+ end
+ self[xpath_key] = attrs.empty? ? nil : attrs
+ end
+
+ protected def parse_text_info(document, status) : Nil
+ if keys = document.xpath_nodes("//#{status}").first?.try(&.children)
+ attrs = {} of String => String
+ keys.each do |data|
+ key = data.name
+ next if key == "text"
+ attrs[key.underscore] = data.text.strip
+ end
+ self[status] = attrs.empty? ? nil : attrs
+ else
+ self[status] = nil
+ end
+ end
+
+ protected def check_success(response)
+ raise "issue with request: #{response.status_code}\n#{response.body}" unless response.success?
+ document = parse_without_namespaces(response.body)
+ status = document.xpath_nodes("//request-status/status").first?.try &.text.strip
+ raise "request failed with #{status}\n#{response.body}" unless status == "OK"
+ sensor_time(document)
+ document
+ end
+
+ protected def sensor_time(document) : Time?
+ if time_text = document.xpath_nodes("//sensor-time").first?.try &.text
+ self[:sensor_time] = Time.parse!(time_text, "%Y-%m-%dT%H:%M:%S%z")
+ end
+ end
+
+ protected def parse_without_namespaces(xml : String)
+ xml = xml.strip
+ document = XML.parse(xml)
+ namespace_node = document.children[0].name == "xml" ? document.children[1].name : document.children[0].name
+ namespaces = document.children[0].namespaces.keys.compact_map { |name| name.starts_with?("xmlns:") ? "#{name[6..-1]}\\:" : nil }
+
+ # Clean up namespaces from node names
+ xml = xml.gsub(Regex.new(namespaces.join("|")), "")
+ # Replace namespace node
+ xml = xml.sub(Regex.new("<#{namespace_node}.+>"), "<#{namespace_node}>")
+
+ # Return the parsed document
+ XML.parse(xml)
+ end
+end
diff --git a/drivers/xovis/sensor_api_spec.cr b/drivers/xovis/sensor_api_spec.cr
new file mode 100644
index 00000000000..6db09bec0fc
--- /dev/null
+++ b/drivers/xovis/sensor_api_spec.cr
@@ -0,0 +1,261 @@
+DriverSpecs.mock_driver "Xovis::SensorAPI" do
+ # =========================
+ # GET TOKEN
+ # =========================
+ retval = exec(:get_token)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |request, response|
+ auth = request.headers["Authorization"]
+ if auth == "Basic YWNjb3VudDpwYXNzd29yZCE="
+ response.status_code = 200
+ response.output << "jwt_token"
+ else
+ response.status_code = 401
+ puts "invalid auth header #{auth}"
+ end
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq("jwt_token")
+
+ # =========================
+ # RESET COUNT
+ # =========================
+ retval = exec(:reset_count)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response.output << %(
+
+ 2020-05-04T12:34:46Z
+
+ OK
+
+ )
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq(true)
+ status["sensor_time"].should eq("2020-05-04T12:34:46Z")
+
+ # =========================
+ # COUNT DATA
+ # =========================
+ retval = exec(:count_data)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response.output << %(
+
+
+
+
+ 0
+ 0
+
+
+
+ 2020-05-05T11:20:47+02:00
+
+ OK
+
+ )
+ end
+
+ # What the function should return (for use in making further requests)
+ line_data = [{
+ "name" => "Line 0",
+ "id" => "0",
+ "sensor-type" => "SINGLE_SENSOR",
+ "counts" => {
+ "fw-count" => 0,
+ "bw-count" => 0,
+ },
+ }]
+ retval.get.should eq(line_data)
+ status["lines"].should eq(line_data)
+
+ # =========================
+ # DEVICE INFO
+ # =========================
+ retval = exec(:device_status)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response.output << <<-XML
+
+
+ OK
+ 2020-05-05T13:11:38+02:00 81551360
+
+ 5
+ AB E
+ B
+ 1.2.12 4.3.1 (5b57718)
+
+ 80:1F:12:73:2F:A4 192.168.1.115
+ S01
+ Test
+ PC2S
+ PC2S
+
+ 49.0
+ 46.0
+
+ 8.0 1.20166 0.0 0.0139272 true
+
+ true 85.823784 8.23138
+ 0.143171 0.98707 0.0720739
+
+ false
+
+ false 492810AF623279442C87D2D65D4A6240 2020-05-05T12:22:10+02:00 false
+
+ tracking 1018951 1 true Europe/Zurich
+
+ OK
+
+ XOVIS-PC
+ 192.168.1.115 255.255.255.0 192.168.1.1
+ 192.168.1.1
+
+ false
+ false
+
+ true 2020-05-05T13:06:58+02:00 91 false
+
+
+ sensor-support.xovis.com:443 true
+ true true true true 2020-05-05T02:11:07+02:00 2020-05-05T02:02:53+02:00false
+
+
+ XML
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq(true)
+ status["version"].should eq({
+ "HW" => "5",
+ "PROD" => "AB",
+ "BOM" => "E",
+ "PCB" => "B",
+ "FW" => "1.2.12",
+ "SW" => "4.3.1 (5b57718)",
+ })
+ status["temperature"].should eq({
+ "die" => "49.0",
+ "housing" => "46.0",
+ })
+ status["sensor"].should eq({
+ "serial-number" => "80:1F:12:73:2F:A4",
+ "ip-address" => "192.168.1.115",
+ "name" => "S01",
+ "group" => "Test",
+ "type" => "PC2S",
+ "device-type" => "PC2S",
+ })
+
+ # =========================
+ # ALIVE CHECK
+ # =========================
+ retval = exec(:is_alive?)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response.output << %(
+
+ 2020-05-04T12:34:46Z
+
+ OK
+
+ )
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq(true)
+
+ # =========================
+ # CAPACITY DATA
+ # =========================
+ retval = exec(:capacity_data)
+
+ # We should request a new token from Floorsense
+ expect_http_request do |_request, response|
+ response.status_code = 200
+ response.output << %(
+
+ 2020-05-05T13:06:06+02:00
+
+ OK
+
+
+ 100
+
+
+ Line 0
+ 0
+ 2020-04-27T11:40:00+02:00
+ 2020-05-05T13:05:00+02:00
+ 9
+
+
+
+
+ 80
+
+
+ Zone 0
+ 0
+ 2020-05-05T12:22:00+02:00
+ 2020-05-05T13:05:00+02:00
+ 1
+
+
+
+
+ 80
+
+
+ Zone 0
+ 0
+ 2020-05-05T12:22:00+02:00
+ 2020-05-05T13:05:00+02:00
+ 1
+
+
+
+ )
+ end
+
+ # What the function should return (for use in making further requests)
+ retval.get.should eq(true)
+ status["line-counts"].should eq([{
+ "name" => "Line 0",
+ "id" => "0",
+ "first-entry" => "2020-04-27T11:40:00+02:00",
+ "last-entry" => "2020-05-05T13:05:00+02:00",
+ "entry-count" => 9,
+ "capacity" => 100,
+ }])
+ status["zone-occupancy-counts"].should eq([{
+ "name" => "Zone 0",
+ "id" => "0",
+ "first-entry" => "2020-05-05T12:22:00+02:00",
+ "last-entry" => "2020-05-05T13:05:00+02:00",
+ "entry-count" => 1,
+ "capacity" => 80,
+ }])
+ status["zone-in-out-counts"].should eq([{
+ "name" => "Zone 0",
+ "id" => "0",
+ "first-entry" => "2020-05-05T12:22:00+02:00",
+ "last-entry" => "2020-05-05T13:05:00+02:00",
+ "entry-count" => 1,
+ "capacity" => 80,
+ }])
+end
diff --git a/drivers/xy_sense/location_service.cr b/drivers/xy_sense/location_service.cr
new file mode 100644
index 00000000000..7da92602659
--- /dev/null
+++ b/drivers/xy_sense/location_service.cr
@@ -0,0 +1,189 @@
+module XYSense; end
+
+require "json"
+require "oauth2"
+require "placeos-driver/interface/locatable"
+
+class XYSense::LocationService < PlaceOS::Driver
+ include Interface::Locatable
+
+ descriptive_name "XY Sense Locations"
+ generic_name :XYLocationService
+ description %(collects desk booking data from the staff API and overlays XY Sense data for visualising on a map)
+
+ accessor area_manager : AreaManagement_1
+ accessor xy_sense : XYSense_1
+ bind XYSense_1, :floors, :floor_details_changed
+
+ default_settings({
+ floor_mappings: {
+ "xy-sense-floor-id": {
+ zone_id: "placeos-zone-id",
+ name: "friendly name for documentation",
+ },
+ },
+ })
+
+ @floor_mappings : Hash(String, NamedTuple(zone_id: String)) = {} of String => NamedTuple(zone_id: String)
+ @zone_filter : Array(String) = [] of String
+
+ def on_load
+ on_update
+ end
+
+ def on_update
+ @floor_mappings = setting(Hash(String, NamedTuple(zone_id: String)), :floor_mappings)
+ @zone_filter = @floor_mappings.map { |_, detail| detail[:zone_id] }
+ end
+
+ # ===================================
+ # Bindings into xy-sense data
+ # ===================================
+ class FloorDetails
+ include JSON::Serializable
+
+ property floor_id : String
+ property floor_name : String
+ property location_id : String
+ property location_name : String
+
+ property spaces : Array(SpaceDetails)
+ end
+
+ class SpaceDetails
+ include JSON::Serializable
+
+ property id : String
+ property name : String
+ property capacity : Int32
+ property category : String
+ end
+
+ class Occupancy
+ include JSON::Serializable
+
+ property status : String
+ property headcount : Int32
+ property space_id : String
+
+ @[JSON::Field(converter: Time::Format.new("%FT%T", Time::Location::UTC))]
+ property collected : Time
+
+ @[JSON::Field(ignore: true)]
+ property! details : SpaceDetails
+ end
+
+ # Floor id => subscription
+ @floor_subscriptions = {} of String => PlaceOS::Driver::Subscriptions::Subscription
+ @space_details = {} of String => SpaceDetails
+ @change_lock = Mutex.new
+
+ protected def floor_details_changed(_sub = nil, payload = nil)
+ @change_lock.synchronize do
+ # Get the floor details from either the status push event or module update
+ floors = payload ? Hash(String, FloorDetails).from_json(payload) : xy_sense.status(Hash(String, FloorDetails), :floors)
+ space_details = {} of String => SpaceDetails
+
+ # work out what we should be watching
+ monitor = {} of String => String
+ floors.each do |floor_id, floor|
+ mapping = @floor_mappings[floor_id]?
+ next unless mapping
+
+ monitor[floor_id] = mapping[:zone_id]
+
+ # track space data
+ floor.spaces.each { |space| space_details[space.id] = space }
+ end
+
+ # unsubscribe from floors we're not interested in
+ existing = @floor_subscriptions.keys
+ desired = monitor.keys
+ (existing - desired).each { |sub| subscriptions.unsubscribe @floor_subscriptions.delete(sub).not_nil! }
+
+ # update to new space details
+ @space_details = space_details
+
+ # Subscribe to new data
+ (desired - existing).each { |floor_id|
+ zone_id = monitor[floor_id]
+ @floor_subscriptions[floor_id] = xy_sense.subscribe(floor_id) do |_sub, message|
+ level_state_change(zone_id, Array(Occupancy).from_json(message))
+ end
+ }
+ end
+ end
+
+ # Zone_id => area => occupancy details
+ @occupancy_mappings : Hash(String, Hash(String, Occupancy)) = {} of String => Hash(String, Occupancy)
+
+ def level_state_change(zone_id : String, spaces : Array(Occupancy))
+ area_occupancy = {} of String => Occupancy
+ spaces.each do |space|
+ space.details = @space_details[space.space_id]
+ area_occupancy[space.details.name] = space
+ end
+ @occupancy_mappings[zone_id] = area_occupancy
+ area_manager.update_available({zone_id})
+ rescue error
+ logger.error(exception: error) { "error updating level #{zone_id} space changes" }
+ end
+
+ # ===================================
+ # Locatable Interface functions
+ # ===================================
+ def locate_user(email : String? = nil, username : String? = nil)
+ logger.debug { "sensor incapable of locating #{email} or #{username}" }
+ [] of Nil
+ end
+
+ def macs_assigned_to(email : String? = nil, username : String? = nil) : Array(String)
+ logger.debug { "sensor incapable of tracking #{email} or #{username}" }
+ [] of String
+ end
+
+ def check_ownership_of(mac_address : String) : OwnershipMAC?
+ logger.debug { "sensor incapable of tracking #{mac_address}" }
+ nil
+ end
+
+ def device_locations(zone_id : String, location : String? = nil)
+ logger.debug { "searching locatable in zone #{zone_id}" }
+ return [] of Nil unless @zone_filter.includes?(zone_id)
+
+ @occupancy_mappings[zone_id].compact_map do |space_name, space|
+ # Assume this means we're looking at a desk
+ capacity = space.details.capacity
+ if capacity == 1
+ next unless space.headcount > 0
+ next if location.presence && location != "desk"
+ {
+ location: :desk,
+ at_location: space.headcount,
+ map_id: space_name,
+ level: zone_id,
+ capacity: capacity,
+
+ xy_sense_space_id: space.space_id,
+ xy_sense_status: space.status,
+ xy_sense_collected: space.collected.to_unix,
+ xy_sense_category: space.details.category,
+ }
+ else
+ next if location.presence && location != "area"
+ {
+ location: :area,
+ at_location: space.headcount,
+ map_id: space_name,
+ level: zone_id,
+ capacity: capacity,
+
+ xy_sense_space_id: space.space_id,
+ xy_sense_status: space.status,
+ xy_sense_collected: space.collected.to_unix,
+ xy_sense_category: space.details.category,
+ }
+ end
+ end
+ end
+end
diff --git a/drivers/xy_sense/location_service_spec.cr b/drivers/xy_sense/location_service_spec.cr
new file mode 100644
index 00000000000..651a4531733
--- /dev/null
+++ b/drivers/xy_sense/location_service_spec.cr
@@ -0,0 +1,76 @@
+DriverSpecs.mock_driver "XYSense::LocationService" do
+ system({
+ XYSense: {XYSense},
+ AreaManagement: {AreaManagement},
+ })
+
+ now = Time.local
+ start = now.at_beginning_of_day.to_unix
+ ending = now.at_end_of_day.to_unix
+
+ resp = exec(:device_locations, "placeos-zone-id").get
+ puts resp
+ resp.should eq([
+ {"location" => "desk", "at_location" => 1, "map_id" => "desk-456", "level" => "placeos-zone-id", "capacity" => 1, "xy_sense_space_id" => "xysense-desk-456-id", "xy_sense_status" => "recentlyOccupied", "xy_sense_collected" => 1605088820, "xy_sense_category" => "Workpoint"},
+ {"location" => "area", "at_location" => 8, "map_id" => "area-567", "level" => "placeos-zone-id", "capacity" => 20, "xy_sense_space_id" => "xysense-area-567-id", "xy_sense_status" => "currentlyOccupied", "xy_sense_collected" => 1605088820, "xy_sense_category" => "Lobby"},
+ ])
+end
+
+class XYSense < DriverSpecs::MockDriver
+ def on_load
+ self[:floors] = {
+ "xy-sense-floor-id" => {
+ floor_id: "xy-sense-floor-id",
+ floor_name: "Fancy floor",
+ location_id: "xysense-building",
+ location_name: "Fancy building",
+ spaces: [{
+ id: "xysense-desk-123-id",
+ name: "desk-123",
+ capacity: 1,
+ category: "Workpoint",
+ },
+ {
+ id: "xysense-desk-456-id",
+ name: "desk-456",
+ capacity: 1,
+ category: "Workpoint",
+ },
+ {
+ id: "xysense-area-567-id",
+ name: "area-567",
+ capacity: 20,
+ category: "Lobby",
+ }],
+ },
+ }
+
+ self["xy-sense-floor-id"] = [
+ {
+ status: "notOccupied",
+ headcount: 0,
+ space_id: "xysense-desk-123-id",
+ collected: "2020-11-11T10:00:20",
+ },
+ {
+ status: "recentlyOccupied",
+ headcount: 1,
+ space_id: "xysense-desk-456-id",
+ collected: "2020-11-11T10:00:20",
+ },
+ {
+ status: "currentlyOccupied",
+ headcount: 8,
+ space_id: "xysense-area-567-id",
+ collected: "2020-11-11T10:00:20",
+ },
+ ]
+ end
+end
+
+class AreaManagement < DriverSpecs::MockDriver
+ def update_available(zones : Array(String))
+ logger.info { "requested update to #{zones}" }
+ nil
+ end
+end
diff --git a/src/models/.keep b/repositories/.keep
similarity index 100%
rename from src/models/.keep
rename to repositories/.keep
diff --git a/shard.lock b/shard.lock
new file mode 100644
index 00000000000..a8cfab018d0
--- /dev/null
+++ b/shard.lock
@@ -0,0 +1,251 @@
+# NOTICE: This lockfile contains some overrides from shard.override.yml
+version: 2.0
+shards:
+ CrystalEmail:
+ git: https://github.com/nephos/crystalemail.git
+ version: 0.2.4
+
+ action-controller:
+ git: https://github.com/spider-gazelle/action-controller.git
+ version: 4.4.1
+
+ active-model:
+ git: https://github.com/spider-gazelle/active-model.git
+ version: 2.0.3
+
+ bindata:
+ git: https://github.com/spider-gazelle/bindata.git
+ version: 1.8.2
+
+ bisect:
+ git: https://github.com/spider-gazelle/bisect.git
+ version: 1.2.1
+
+ connect-proxy:
+ git: https://github.com/spider-gazelle/connect-proxy.git
+ version: 1.2.1
+
+ crc16:
+ git: https://github.com/maiha/crc16.cr.git
+ version: 0.1.0
+
+ cron_parser:
+ git: https://github.com/kostya/cron_parser.git
+ version: 0.4.0
+
+ db:
+ git: https://github.com/crystal-lang/crystal-db.git
+ version: 0.10.1
+
+ debug:
+ git: https://github.com/sija/debug.cr.git
+ version: 2.0.1
+
+ email:
+ git: https://github.com/arcage/crystal-email.git
+ version: 0.6.2
+
+ etcd:
+ git: https://github.com/place-labs/crystal-etcd.git
+ version: 1.2.4
+
+ exec_from:
+ git: https://github.com/place-labs/exec_from.git
+ version: 1.3.0
+
+ future:
+ git: https://github.com/crystal-community/future.cr.git
+ version: 1.0.0
+
+ google:
+ git: https://github.com/placeos/google.git
+ version: 3.0.0
+
+ habitat:
+ git: https://github.com/luckyframework/habitat.git
+ version: 0.4.7
+
+ hound-dog:
+ git: https://github.com/place-labs/hound-dog.git
+ version: 2.9.0
+
+ http-params-serializable:
+ git: https://github.com/caspiano/http-params-serializable.git
+ version: 0.4.1+git.commit.c87119117af3d2db11af22ad799ccf77072e6a8b
+
+ inactive-support:
+ git: https://github.com/spider-gazelle/inactive-support.git
+ version: 0.1.1
+
+ ipaddress:
+ git: https://github.com/sija/ipaddress.cr.git
+ version: 0.2.1
+
+ json_mapping:
+ git: https://github.com/crystal-lang/json_mapping.cr.git
+ version: 0.1.0
+
+ jwt:
+ git: https://github.com/crystal-community/jwt.git
+ version: 1.5.1
+
+ link-header:
+ git: https://github.com/spider-gazelle/link-header.git
+ version: 1.0.2
+
+ log_helper:
+ git: https://github.com/spider-gazelle/log_helper.git
+ version: 1.0.3
+
+ lucky_router:
+ git: https://github.com/luckyframework/lucky_router.git
+ version: 0.4.2
+
+ mqtt:
+ git: https://github.com/spider-gazelle/crystal-mqtt.git
+ version: 1.1.1
+
+ murmur3:
+ git: https://github.com/aca-labs/murmur3.git
+ version: 0.1.1+git.commit.7cbe25c0ca8d052c9d98c377c824dcb0e038c790
+
+ neuroplastic:
+ git: https://github.com/spider-gazelle/neuroplastic.git
+ version: 1.7.1
+
+ ntlm:
+ git: https://github.com/spider-gazelle/ntlm.git
+ version: 1.0.1
+
+ office365:
+ git: https://github.com/placeos/office365.git
+ version: 1.13.2
+
+ openssl_ext:
+ git: https://github.com/spider-gazelle/openssl_ext.git
+ version: 2.1.4
+
+ oq:
+ git: https://github.com/blacksmoke16/oq.git
+ version: 1.2.0
+
+ pars:
+ git: https://github.com/place-labs/pars.git
+ version: 1.1.0+git.commit.16bf2c909110cc148818f7af298afeded538123c
+
+ pinger:
+ git: https://github.com/spider-gazelle/pinger.git
+ version: 1.1.1
+
+ place_calendar:
+ git: https://github.com/placeos/calendar.git
+ version: 4.9.3
+
+ placeos:
+ git: https://github.com/placeos/crystal-client.git
+ version: 2.2.0
+
+ placeos-compiler:
+ git: https://github.com/placeos/compiler.git
+ version: 3.5.0
+
+ placeos-driver:
+ git: https://github.com/placeos/driver.git
+ version: 5.1.1
+
+ placeos-models:
+ git: https://github.com/placeos/models.git
+ version: 4.14.1
+
+ pool:
+ git: https://github.com/ysbaddaden/pool.git
+ version: 0.2.4
+
+ priority-queue:
+ git: https://github.com/spider-gazelle/priority-queue.git
+ version: 1.0.1
+
+ promise:
+ git: https://github.com/spider-gazelle/promise.git
+ version: 2.2.2
+
+ qr-code:
+ git: https://github.com/spider-gazelle/qr-code.git
+ version: 1.0.2
+
+ redis:
+ git: https://github.com/maiha/crystal-redis.git
+ version: 2.6.0
+
+ redis-cluster:
+ git: https://github.com/caspiano/redis-cluster.cr.git
+ version: 0.8.4
+
+ rendezvous-hash:
+ git: https://github.com/caspiano/rendezvous-hash.git
+ version: 0.3.1
+
+ responsible:
+ git: https://github.com/place-labs/responsible.git
+ version: 1.2.3
+
+ rethinkdb:
+ git: https://github.com/kingsleyh/crystal-rethinkdb.git
+ version: 0.2.3
+
+ rethinkdb-orm:
+ git: https://github.com/spider-gazelle/rethinkdb-orm.git
+ version: 3.2.4
+
+ retriable: # Overridden
+ git: https://github.com/sija/retriable.cr.git
+ version: 0.2.3
+
+ rwlock:
+ git: https://github.com/spider-gazelle/readers-writer.git
+ version: 1.0.7
+
+ s2_cells:
+ git: https://github.com/spider-gazelle/s2_cells.git
+ version: 1.0.1
+
+ simple_retry:
+ git: https://github.com/spider-gazelle/simple_retry.git
+ version: 1.1.1
+
+ ssh2:
+ git: https://github.com/spider-gazelle/ssh2.cr.git
+ version: 1.5.2
+
+ stomp:
+ git: https://github.com/spider-gazelle/stomp.git
+ version: 1.0.1
+
+ stumpy_core:
+ git: https://github.com/stumpycr/stumpy_core.git
+ version: 1.9.1
+
+ stumpy_png:
+ git: https://github.com/stumpycr/stumpy_png.git
+ version: 5.0.1
+
+ tasker:
+ git: https://github.com/spider-gazelle/tasker.git
+ version: 2.0.3
+
+ telnet:
+ git: https://github.com/spider-gazelle/telnet.cr.git
+ version: 1.0.2
+
+ tokenizer:
+ git: https://github.com/spider-gazelle/tokenizer.git
+ version: 1.1.1
+
+ ulid:
+ git: https://github.com/place-labs/ulid.git
+ version: 0.1.2
+
+ yaml_mapping:
+ git: https://github.com/crystal-lang/yaml_mapping.cr.git
+ version: 0.1.0
+
diff --git a/shard.override.yml b/shard.override.yml
new file mode 100644
index 00000000000..6c8a374874c
--- /dev/null
+++ b/shard.override.yml
@@ -0,0 +1,4 @@
+# NOTE:: here because of a shards ambiguous sources error
+dependencies:
+ retriable:
+ git: https://github.com/sija/retriable.cr.git
diff --git a/shard.yml b/shard.yml
index f1cea9bcc5b..10d29d7e401 100644
--- a/shard.yml
+++ b/shard.yml
@@ -1,18 +1,90 @@
-name: app
-version: 1.0.0
+name: drivers
+version: 2.2.0
+
+# compile target
+targets:
+ test-harness:
+ main: src/app.cr
+ report:
+ main: src/report.cr
dependencies:
action-controller:
github: spider-gazelle/action-controller
- active-model:
- github: spider-gazelle/active-model
+ version: ~> 4.4
- # https://github.com/jeromegn/kilt
- # Generic template interface for Crystal
- kilt:
- github: jeromegn/kilt
+ email:
+ github: arcage/crystal-email
-# compile target
-targets:
- app:
- main: src/app.cr
+ exec_from:
+ github: place-labs/exec_from
+
+ inactive-support:
+ github: spider-gazelle/inactive-support
+
+ jwt:
+ github: crystal-community/jwt
+
+ pinger:
+ github: spider-gazelle/pinger
+
+ ntlm:
+ github: spider-gazelle/ntlm
+
+ link-header:
+ github: spider-gazelle/link-header
+
+ place_calendar:
+ github: PlaceOS/calendar
+
+ placeos-compiler:
+ github: placeos/compiler
+ version: ~> 3.5
+
+ placeos-driver:
+ github: placeos/driver
+ version: ~> 5.0
+
+ # The PlaceOS API client
+ placeos:
+ github: placeos/crystal-client
+ version: ~> 2.2
+
+ # HTTP Client helper
+ responsible:
+ github: place-labs/responsible
+
+ rwlock:
+ github: spider-gazelle/readers-writer
+
+ # Driver deps:
+ telnet:
+ github: spider-gazelle/telnet.cr
+
+ pars:
+ github: place-labs/pars
+ branch: master
+
+ # For lat lon location indexing
+ s2_cells:
+ github: spider-gazelle/s2_cells
+
+ qr-code:
+ github: spider-gazelle/qr-code
+
+ # For QR .png file export
+ stumpy_png:
+ github: stumpycr/stumpy_png
+
+ # the STOMP protocol
+ stomp:
+ github: spider-gazelle/stomp
+
+ # MQTT protocol
+ mqtt:
+ github: spider-gazelle/crystal-mqtt
+
+ # XML to JSON convertor
+ oq:
+ github: blacksmoke16/oq
+ version: ~> 1.2
diff --git a/spec/build_spec.cr b/spec/build_spec.cr
new file mode 100644
index 00000000000..9b93a15690c
--- /dev/null
+++ b/spec/build_spec.cr
@@ -0,0 +1,44 @@
+require "./spec_helper"
+
+module PlaceOS::Drivers::Api
+ describe Build do
+ with_server do
+ it "should list drivers" do
+ result = curl("GET", "/build")
+ drivers = Array(String).from_json(result.body)
+ (drivers.size > 0).should be_true
+ drivers.includes?("drivers/place/spec_helper.cr").should be_true
+ end
+
+ it "should build a driver" do
+ result = curl("POST", "/build?driver=drivers/place/spec_helper.cr")
+ result.status_code.should eq(201)
+ end
+
+ it "should list compiled versions" do
+ result = curl("GET", "/build/drivers%2Fplace%2Fspec_helper.cr/")
+ result.status_code.should eq(200)
+ drivers = Array(String).from_json(result.body)
+ drivers[0].starts_with?("drivers_place_spec_helper_").should be_true
+ end
+
+ it "should list possible versions" do
+ result = curl("GET", "/build/drivers%2Fplace%2Fspec_helper.cr/commits")
+
+ result.status_code.should eq(200)
+ commits = JSON.parse(result.body)
+ commits.size.should eq(1)
+ end
+
+ it "should delete all compiled versions of a driver" do
+ result = curl("DELETE", "/build/drivers%2Fplace%2Fspec_helper.cr/")
+ result.status_code.should eq(200)
+
+ result = curl("GET", "/build/drivers%2Fplace%2Fspec_helper.cr/")
+ result.status_code.should eq(200)
+ drivers = Array(String).from_json(result.body)
+ drivers.size.should eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/compiler_spec.cr b/spec/compiler_spec.cr
new file mode 100644
index 00000000000..1de0658d1a6
--- /dev/null
+++ b/spec/compiler_spec.cr
@@ -0,0 +1,16 @@
+require "./spec_helper"
+require "file_utils"
+
+module PlaceOS::Drivers
+ describe Compiler do
+ with_server do
+ it "should compile a private driver using the build API" do
+ PlaceOS::Compiler.clone_and_install("private_drivers", "https://github.com/placeos/private-drivers.git")
+ File.file?(File.expand_path("./repositories/private_drivers/drivers/place/private_helper.cr")).should be_true
+
+ result = curl("POST", "/build?repository=private_drivers&driver=drivers/place/private_helper.cr")
+ result.status_code.should eq(201)
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
index e008dcfd222..c86d37333ab 100644
--- a/spec/spec_helper.cr
+++ b/spec/spec_helper.cr
@@ -1,7 +1,20 @@
require "spec"
# Your application config
+# If you have a testing environment, replace this with a test config file
require "../src/config"
# Helper methods for testing controllers (curl, with_server, context)
require "../lib/action-controller/spec/curl_context"
+
+require "placeos-compiler"
+
+Spec.before_suite do
+ ::Log.setup("*", :debug, ActionController.default_backend)
+
+ # Clone the private drivers
+ PlaceOS::Compiler.clone_and_install(
+ "private_drivers",
+ "https://github.com/placeos/private-drivers"
+ )
+end
diff --git a/spec/test_spec.cr b/spec/test_spec.cr
new file mode 100644
index 00000000000..694b408d8a5
--- /dev/null
+++ b/spec/test_spec.cr
@@ -0,0 +1,19 @@
+require "./spec_helper"
+
+module PlaceOS::Drivers::Api
+ describe Test do
+ with_server do
+ it "should list drivers" do
+ result = curl("GET", "/test?repository=private_drivers")
+ drivers = Array(String).from_json(result.body)
+ (drivers.size > 0).should be_true
+ drivers.includes?("drivers/place/private_helper_spec.cr").should be_true
+ end
+
+ it "should build a driver" do
+ result = curl("POST", "/test?repository=private_drivers&driver=drivers/place/private_helper.cr&spec=drivers/place/private_helper_spec.cr&force=true")
+ result.status_code.should eq(200)
+ end
+ end
+ end
+end
diff --git a/spec/welcome_spec.cr b/spec/welcome_spec.cr
deleted file mode 100644
index 5666e9ad6e3..00000000000
--- a/spec/welcome_spec.cr
+++ /dev/null
@@ -1,25 +0,0 @@
-require "./spec_helper"
-
-describe Welcome do
- # ==============
- # Unit Testing
- # ==============
- it "should generate a date string" do
- # instantiate the controller you wish to unit test
- welcome = Welcome.new(context("GET", "/"))
-
- # Test the instance methods of the controller
- welcome.time_now.should contain("GMT")
- end
-
- # ==============
- # Test Responses
- # ==============
- with_server do
- it "should welcome you" do
- result = curl("GET", "/")
- result.body.should eq("\n\nWelcome \nYou're riding on Spider-Gazelle!\n\n")
- result.headers["Date"]?.nil?.should eq(false)
- end
- end
-end
diff --git a/src/app.cr b/src/app.cr
index 4a6e7bbdbb3..c95bda561ba 100644
--- a/src/app.cr
+++ b/src/app.cr
@@ -4,14 +4,25 @@ require "./config"
# Server defaults
port = 3000
host = "127.0.0.1"
+cluster = false
+process_count = 1
# Command line options
-OptionParser.parse! do |parser|
+OptionParser.parse(ARGV.dup) do |parser|
parser.banner = "Usage: #{PROGRAM_NAME} [arguments]"
parser.on("-b HOST", "--bind=HOST", "Specifies the server host") { |h| host = h }
parser.on("-p PORT", "--port=PORT", "Specifies the server port") { |p| port = p.to_i }
+ # There should only ever be a single instance of this process
+ # as we don't want concurrent access occuring
+ # (technically git allows concurrent repo access however you may experience unexpected
+ # results as file revisions are changed while compiling etc)
+ # parser.on("-w COUNT", "--workers=COUNT", "Specifies the number of processes to handle requests") do |w|
+ # cluster = true
+ # process_count = w.to_i
+ # end
+
parser.on("-r", "--routes", "List the application routes") do
ActionController::Server.print_routes
exit 0
@@ -32,15 +43,24 @@ end
puts "Launching #{APP_NAME} v#{VERSION}"
server = ActionController::Server.new(port, host)
-# Detect ctr-c to shutdown gracefully
-Signal::INT.trap do
+# Start clustering
+server.cluster(process_count, "-w", "--workers") if cluster
+
+terminate = Proc(Signal, Nil).new do |signal|
puts " > terminating gracefully"
- server.close
+ spawn { server.close }
+ signal.ignore
end
+# Detect ctr-c to shutdown gracefully
+Signal::INT.trap &terminate
+# Docker containers use the term signal
+Signal::TERM.trap &terminate
+
# Start the server
-puts "Listening on tcp://#{host}:#{port}"
-server.run
+server.run do
+ puts "Listening on #{server.print_addresses}"
+end
# Shutdown message
puts "#{APP_NAME} leaps through the veldt\n"
diff --git a/src/build.cr b/src/build.cr
new file mode 100644
index 00000000000..bbbddaf0bf2
--- /dev/null
+++ b/src/build.cr
@@ -0,0 +1,8 @@
+{% if env("COMPILE_DRIVER").ends_with?("_spec.cr") %}
+ require "placeos-driver/driver-specs/runner"
+{% else %}
+ require "placeos-driver"
+{% end %}
+
+# Dynamically require the desired driver
+{{ ("require \"../" + env("COMPILE_DRIVER") + "\"").id }}
diff --git a/src/config.cr b/src/config.cr
index f48255a4f35..bb4e866c437 100644
--- a/src/config.cr
+++ b/src/config.cr
@@ -1,21 +1,48 @@
# Application dependencies
require "action-controller"
-require "active-model"
+require "placeos-compiler"
# Application code
require "./controllers/application"
require "./controllers/*"
-require "./models/*"
# Server required after application controllers
require "action-controller/server"
+PROD = ENV["SG_ENV"]? == "production"
+
+# Configure logging
+Log.setup do |config|
+ config.bind "*", :warn, ActionController.default_backend
+ config.bind "action-controller.*", :info, ActionController.default_backend
+end
+
+filters = PROD ? ["bearer_token", "secret", "password"] : [] of String
+
+# Add handlers that should run before your application
+ActionController::Server.before(
+ HTTP::ErrorHandler.new(PROD),
+ ActionController::LogHandler.new(filters, ActionController::LogHandler::Event.all),
+)
+
+# Optional support for serving of static assests
+static_file_path = ENV["PUBLIC_WWW_PATH"]? || "./www"
+if File.directory?(static_file_path)
+ # Optionally add additional mime types
+ ::MIME.register(".yaml", "text/yaml")
+
+ # Check for files if no paths matched in your application
+ ActionController::Server.before(
+ ::HTTP::StaticFileHandler.new(static_file_path, directory_listing: false)
+ )
+end
+
# Configure session cookies
-# NOTE:: Change these from defaults
-ActionController::Session.configure do
- settings.key = "_spider_gazelle_"
- settings.secret = "4f74c0b358d5bab4000dd3c75465dc2c"
+ActionController::Session.configure do |settings|
+ settings.key = ENV["COOKIE_SESSION_KEY"]? || "_spider_gazelle_"
+ settings.secret = ENV["COOKIE_SESSION_SECRET"]? || "4f74c0b358d5bab4000dd3c75465dc2c"
+ settings.secure = PROD
end
-APP_NAME = "Spider-Gazelle"
-VERSION = "1.0.0"
+APP_NAME = "Drivers Test Harness"
+VERSION = `shards version`
diff --git a/src/controllers/application.cr b/src/controllers/application.cr
index 9bdbc79b557..a38cd60a6fe 100644
--- a/src/controllers/application.cr
+++ b/src/controllers/application.cr
@@ -1,15 +1,19 @@
-# Require kilt for template support
-require "kilt"
+require "uuid"
+require "action-controller"
-abstract class Application < ActionController::Base
- before_action :set_date_header
+module PlaceOS::Drivers::Api
+ abstract class Application < ActionController::Base
+ before_action :set_request_id
- # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date
- def time_now
- Time.utc_now.to_s("%a, %d %b %Y %H:%M:%S GMT")
- end
+ # Support request tracking
+ def set_request_id
+ Log.context.set(client_ip: client_ip)
+ response.headers["X-Request-ID"] = Log.context.metadata[:request_id].as_s
+ end
- def set_date_header
- response.headers["Date"] = time_now
+ # Builds and validates the selected repository
+ def get_repository_path
+ Compiler::Helper.get_repository_path(params["repository"]?)
+ end
end
end
diff --git a/src/controllers/build.cr b/src/controllers/build.cr
new file mode 100644
index 00000000000..598cbafb6b7
--- /dev/null
+++ b/src/controllers/build.cr
@@ -0,0 +1,87 @@
+require "./application"
+
+module PlaceOS::Drivers::Api
+ class Build < Application
+ base "/build"
+
+ # list the available files
+ def index
+ compiled = params["compiled"]?
+ if compiled
+ render json: PlaceOS::Compiler.compiled_drivers
+ else
+ result = Dir.cd(get_repository_path) do
+ Dir.glob("drivers/**/*.cr").reject! do |path|
+ path.ends_with?("_spec.cr") || !File.read_lines(path).any? &.includes?("< PlaceOS::Driver")
+ end
+ end
+
+ render json: result
+ end
+ end
+
+ def show
+ driver_source = URI.decode(params["id"])
+ render json: PlaceOS::Compiler.compiled_drivers(driver_source)
+ end
+
+ # grab the list of available repositories
+ get "/repositories" do
+ render json: PlaceOS::Compiler.repositories
+ end
+
+ # grab the list of available versions of file / which are built
+ get "/:id/commits" do
+ driver_source = URI.decode(params["id"])
+ count = (params["count"]? || 50).to_i
+
+ render json: PlaceOS::Compiler::GitCommands.commits(driver_source, count, get_repository_path)
+ end
+
+ # Commits at repo level
+ get "/repository_commits" do
+ count = (params["count"]? || 50).to_i
+ render json: PlaceOS::Compiler::GitCommands.repository_commits(get_repository_path, count)
+ end
+
+ # build a drvier, optionally based on the version specified
+ def create
+ driver = params["driver"]
+ commit = params["commit"]? || "HEAD"
+
+ result = PlaceOS::Compiler.build_driver(driver, commit, get_repository_path)
+
+ if result[:exit_status] == 0
+ render :not_acceptable, text: result[:output] unless File.exists?(result[:executable])
+ else
+ render :not_acceptable, text: result[:output]
+ end
+
+ response.headers["Location"] = "/build/#{URI.encode_www_form(driver)}"
+ head :created
+ end
+
+ # delete a built driver
+ def destroy
+ driver_source = URI.decode(params["id"])
+ commit = params["commit"]?
+
+ # Check repository to prevent abuse (don't want to delete the wrong thing)
+ repository = get_repository_path
+ PlaceOS::Compiler::GitCommands.checkout(driver_source, commit || "HEAD", repository) do
+ head :not_found unless File.exists?(File.join(repository, driver_source))
+ end
+
+ files = if commit
+ [Compiler.executable_name(driver_source, commit, nil)]
+ else
+ PlaceOS::Compiler.compiled_drivers(driver_source)
+ end
+
+ files.each do |file|
+ File.delete File.join(PlaceOS::Compiler.bin_dir, file)
+ end
+ head :ok
+ end
+ end
+end
diff --git a/src/controllers/test.cr b/src/controllers/test.cr
new file mode 100644
index 00000000000..53ea161fe3b
--- /dev/null
+++ b/src/controllers/test.cr
@@ -0,0 +1,144 @@
+require "./application"
+
+module PlaceOS::Drivers::Api
+ class Test < Application
+ base "/test"
+
+ before_action :ensure_driver_compiled, only: [:run_spec, :create]
+ before_action :ensure_spec_compiled, only: [:run_spec, :create]
+ @driver_path : String = ""
+ @spec_path : String = ""
+
+ PLACE_DRIVERS_DIR = "../../#{Dir.current.split("/")[-1]}"
+
+ # Specs available
+ def index
+ result = [] of String
+ Dir.cd(get_repository_path) do
+ Dir.glob("drivers/**/*_spec.cr") { |file| result << file }
+ end
+ render json: result
+ end
+
+ # grab the list of available versions of the spec file
+ get "/:id/commits" do
+ spec = URI.decode(params["id"])
+ count = (params["count"]? || 50).to_i
+
+ render json: Compiler::GitCommands.commits(spec, count, get_repository_path)
+ end
+
+ # Run the spec and return success if the exit status is 0
+ def create
+ debug = params["debug"]? == "true"
+
+ io = IO::Memory.new
+ exit_code = launch_spec(io, debug)
+
+ render :not_acceptable, text: io.to_s if exit_code != 0
+ render text: io.to_s
+ end
+
+ # WS watch the output from running specs
+ ws "/run_spec", :run_spec do |socket|
+ debug = params["debug"]? == "true"
+
+ # Run the spec and pipe all the IO down the websocket
+ spawn { pipe_spec(socket, debug) }
+ end
+
+ def pipe_spec(socket, debug)
+ output, output_writer = IO.pipe
+ spawn { launch_spec(output_writer, debug) }
+
+ # Read data coming in from the IO and send it down the websocket
+ raw_data = Bytes.new(1024)
+ begin
+ while !output.closed?
+ bytes_read = output.read(raw_data)
+ break if bytes_read == 0 # IO was closed
+ socket.send String.new(raw_data[0, bytes_read])
+ end
+ rescue IO::Error
+ # Input stream closed. This should only occur on termination
+ end
+
+ # Once the process exits, close the websocket
+ socket.close
+ end
+
+ GDB_SERVER_PORT = ENV["GDB_SERVER_PORT"]? || "4444"
+
+ def launch_spec(io, debug)
+ io << "\nLaunching spec runner\n"
+
+ if debug
+ exit_code = Process.run(
+ "gdbserver",
+ {"0.0.0.0:#{GDB_SERVER_PORT}", @spec_path},
+ {"SPEC_RUN_DRIVER" => @driver_path},
+ input: Process::Redirect::Close,
+ output: io,
+ error: io
+ ).exit_code
+ io << "spec runner exited with #{exit_code}\n"
+ io.close
+ exit_code
+ else
+ exit_code = Process.run(
+ @spec_path,
+ nil,
+ {"SPEC_RUN_DRIVER" => @driver_path},
+ input: Process::Redirect::Close,
+ output: io,
+ error: io
+ ).exit_code
+ io << "spec runner exited with #{exit_code}\n"
+ io.close
+ exit_code
+ end
+ end
+
+ def ensure_driver_compiled
+ driver = params["driver"]
+ repository = get_repository_path
+ commit = params["commit"]? || "HEAD"
+
+ driver_path = Compiler.is_built?(driver, commit, repository)
+
+ # Build the driver if has not been compiled yet
+ debug = params["debug"]?
+ if driver_path.nil? || params["force"]? || debug
+ result = Compiler.build_driver(driver, commit, repository, debug: !!debug)
+ output = result[:output].strip
+
+ render :not_acceptable, text: output if result[:exit_status] != 0 || !File.exists?(result[:executable])
+
+ driver_path = Compiler.is_built?(driver, commit, repository)
+ end
+
+ # raise an error if the driver still does not exist
+ @driver_path = driver_path.not_nil!
+ end
+
+ def ensure_spec_compiled
+ spec = params["spec"]
+ repository = get_repository_path
+ spec_commit = params["spec_commit"]? || "HEAD"
+
+ spec_path = Compiler.is_built?(spec, spec_commit, repository)
+
+ debug = params["debug"]?
+ if spec_path.nil? || params["force"]? || debug
+ result = Compiler.build_driver(spec, spec_commit, repository, debug: !!debug)
+ output = result[:output].strip
+
+ render :not_acceptable, text: output if result[:exit_status] != 0 || !File.exists?(result[:executable])
+
+ spec_path = Compiler.is_built?(spec, spec_commit, repository)
+ end
+
+ @spec_path = spec_path.not_nil!
+ end
+ end
+end
diff --git a/src/controllers/welcome.cr b/src/controllers/welcome.cr
index 7695e78845b..ef4543f9117 100644
--- a/src/controllers/welcome.cr
+++ b/src/controllers/welcome.cr
@@ -1,17 +1,17 @@
-class Welcome < Application
- base "/"
+require "./application"
- def index
- welcome_text = "You're riding on Spider-Gazelle!"
+module PlaceOS::Drivers::Api
+ class Welcome < Application
+ base "/"
- respond_with do
- html Kilt.render("src/views/welcome.ecr")
- text "Welcome, #{welcome_text}"
- json({welcome: welcome_text})
- xml do
- XML.build(indent: " ") do |xml|
- xml.element("welcome") { xml.text welcome_text }
- end
+ STATIC_FILE_PATH = File.join(File.expand_path(ENV["PUBLIC_WWW_PATH"]? || "./www"), "index.html")
+
+ def index
+ file_path = STATIC_FILE_PATH
+ response.content_type = MIME.from_filename(file_path, "application/octet-stream")
+ response.content_length = File.size(file_path)
+ File.open(file_path) do |file|
+ IO.copy(file, response)
end
end
end
diff --git a/src/report.cr b/src/report.cr
new file mode 100644
index 00000000000..9fc8fe6f0d4
--- /dev/null
+++ b/src/report.cr
@@ -0,0 +1,139 @@
+require "uri"
+require "http"
+require "json"
+require "colorize"
+require "option_parser"
+
+host = "localhost"
+port = 8080
+repo = ""
+
+OptionParser.parse(ARGV.dup) do |parser|
+ parser.banner = "Usage: #{PROGRAM_NAME} [arguments]"
+
+ parser.on("-h HOST", "--host=HOST", "Specifies the server host") { |h| host = h }
+ parser.on("-p PORT", "--port=PORT", "Specifies the server port") { |p| port = p.to_i }
+ parser.on("-r REPO", "--repo=REPO", "Specifies the repository to report on") { |r| repo = r }
+end
+
+puts "running report against #{host}:#{port} (#{repo.blank? ? "default" : repo} repository)"
+
+# ================
+# driver discovery
+# ================
+print "discovering drivers... "
+
+response = HTTP::Client.get "http://#{host}:#{port}/build"
+if !response.success?
+ puts "failed to obtain driver list"
+ exit 1
+end
+
+drivers = Array(String).from_json(response.body)
+puts "found #{drivers.size}"
+# ================
+
+# ==============
+# spec discovery
+# ==============
+print "locating specs... "
+
+response = HTTP::Client.get "http://#{host}:#{port}/test"
+if !response.success?
+ puts "failed to obtain spec list"
+ exit 2
+end
+
+specs = Array(String).from_json(response.body)
+puts "found #{specs.size}"
+# ==============
+
+compile_only = [] of String
+failed = [] of String
+no_compile = [] of String
+timeout = [] of String
+
+tested = 0
+success = 0
+
+# detect ctrl-c, complete current work and output report early
+skip_remaining = false
+Signal::INT.trap do |signal|
+ skip_remaining = true
+ signal.ignore
+end
+
+drivers.each do |driver|
+ break if skip_remaining
+
+ spec = "#{driver.rchop(".cr")}_spec.cr"
+ if !specs.includes? spec
+ compile_only << driver
+ next
+ end
+
+ tested += 1
+ print "testing #{driver}..."
+
+ params = URI::Params.new({
+ "driver" => [driver],
+ "spec" => [spec],
+ "force" => ["true"],
+ })
+ params["repository"] = repo unless repo.blank?
+ uri = URI.new(path: "/test", query: params)
+
+ client = HTTP::Client.new(host, port)
+ client.read_timeout = 6.minutes
+ begin
+ response = client.post(uri.to_s)
+ if response.success?
+ success += 1
+ puts " passed".colorize.green
+ else
+ # keep travis alive
+ print " "
+
+ # a spec not passing isn't as critical as a driver not compiling
+ if client.post("/build?driver=#{driver}").success?
+ puts "failed".colorize.red
+ failed << driver
+ else
+ puts "failed to compile!".colorize.red
+ no_compile << driver
+ end
+ end
+ rescue IO::TimeoutError
+ puts "failed with timeout".colorize.red
+ timeout << driver
+ end
+end
+
+compile_only.each do |driver|
+ break if skip_remaining
+
+ print "compile #{driver}... "
+ client = HTTP::Client.new(host, port)
+ client.read_timeout = 6.minutes
+ begin
+ response = client.post("/build?driver=#{driver}")
+ if response.success?
+ success += 1
+ puts "builds".colorize.green
+ else
+ puts "failed to compile!".colorize.red
+ no_compile << driver
+ end
+ rescue IO::TimeoutError
+ puts "failed with timeout".colorize.red
+ timeout << driver
+ end
+end
+
+# ==============
+# output report
+# ==============
+puts "\n\nspec failures:\n * #{failed.join("\n * ")}" if !failed.empty?
+puts "\n\nspec timeouts:\n * #{timeout.join("\n * ")}" if !timeout.empty?
+puts "\n\nfailed to compile:\n * #{no_compile.join("\n * ")}" if !no_compile.empty?
+puts "\n\n#{tested} drivers, #{failed.size + no_compile.size} failures, #{timeout.size} timeouts, #{compile_only.size} without spec"
diff --git a/src/views/welcome.ecr b/src/views/welcome.ecr
deleted file mode 100644
index 69f2e46c394..00000000000
--- a/src/views/welcome.ecr
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-Welcome
-<%= welcome_text %>
-
diff --git a/www/3rdpartylicenses.txt b/www/3rdpartylicenses.txt
new file mode 100644
index 00000000000..1e695fbab68
--- /dev/null
+++ b/www/3rdpartylicenses.txt
@@ -0,0 +1,374 @@
+@angular/animations
+MIT
+
+@angular/cdk
+MIT
+The MIT License
+
+Copyright (c) 2021 Google LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+@angular/common
+MIT
+
+@angular/core
+MIT
+
+@angular/forms
+MIT
+
+@angular/material
+MIT
+The MIT License
+
+Copyright (c) 2021 Google LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+@angular/platform-browser
+MIT
+
+@angular/router
+MIT
+
+@angular/service-worker
+MIT
+
+css-loader
+MIT
+Copyright JS Foundation and other contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+rxjs
+Apache-2.0
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+string-similarity-js
+MIT
+Copyright 2018 Stephen Brown
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+tslib
+0BSD
+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+xterm
+MIT
+Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
+Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
+Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+zone.js
+MIT
+The MIT License
+
+Copyright (c) 2010-2020 Google LLC. http://angular.io/license
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/www/MaterialIcons-Regular.4674f8ded773cb03e824.eot b/www/MaterialIcons-Regular.4674f8ded773cb03e824.eot
new file mode 100644
index 00000000000..70508ebabc9
Binary files /dev/null and b/www/MaterialIcons-Regular.4674f8ded773cb03e824.eot differ
diff --git a/www/MaterialIcons-Regular.5e7382c63da0098d634a.ttf b/www/MaterialIcons-Regular.5e7382c63da0098d634a.ttf
new file mode 100644
index 00000000000..7015564ad16
Binary files /dev/null and b/www/MaterialIcons-Regular.5e7382c63da0098d634a.ttf differ
diff --git a/www/MaterialIcons-Regular.83bebaf37c09c7e1c3ee.woff b/www/MaterialIcons-Regular.83bebaf37c09c7e1c3ee.woff
new file mode 100644
index 00000000000..b648a3eea2d
Binary files /dev/null and b/www/MaterialIcons-Regular.83bebaf37c09c7e1c3ee.woff differ
diff --git a/www/MaterialIcons-Regular.cff684e59ffb052d72cb.woff2 b/www/MaterialIcons-Regular.cff684e59ffb052d72cb.woff2
new file mode 100644
index 00000000000..9fa21125208
Binary files /dev/null and b/www/MaterialIcons-Regular.cff684e59ffb052d72cb.woff2 differ
diff --git a/www/assets/icons/icon-128x128.png b/www/assets/icons/icon-128x128.png
new file mode 100644
index 00000000000..9f9241f0be4
Binary files /dev/null and b/www/assets/icons/icon-128x128.png differ
diff --git a/www/assets/icons/icon-144x144.png b/www/assets/icons/icon-144x144.png
new file mode 100644
index 00000000000..4a5f8c16389
Binary files /dev/null and b/www/assets/icons/icon-144x144.png differ
diff --git a/www/assets/icons/icon-152x152.png b/www/assets/icons/icon-152x152.png
new file mode 100644
index 00000000000..34a1a8d6458
Binary files /dev/null and b/www/assets/icons/icon-152x152.png differ
diff --git a/www/assets/icons/icon-192x192.png b/www/assets/icons/icon-192x192.png
new file mode 100644
index 00000000000..9172e5dd29e
Binary files /dev/null and b/www/assets/icons/icon-192x192.png differ
diff --git a/www/assets/icons/icon-384x384.png b/www/assets/icons/icon-384x384.png
new file mode 100644
index 00000000000..e54e8d3eafe
Binary files /dev/null and b/www/assets/icons/icon-384x384.png differ
diff --git a/www/assets/icons/icon-512x512.png b/www/assets/icons/icon-512x512.png
new file mode 100644
index 00000000000..51ee297df1c
Binary files /dev/null and b/www/assets/icons/icon-512x512.png differ
diff --git a/www/assets/icons/icon-72x72.png b/www/assets/icons/icon-72x72.png
new file mode 100644
index 00000000000..2814a3f30ca
Binary files /dev/null and b/www/assets/icons/icon-72x72.png differ
diff --git a/www/assets/icons/icon-96x96.png b/www/assets/icons/icon-96x96.png
new file mode 100644
index 00000000000..d271025c4f2
Binary files /dev/null and b/www/assets/icons/icon-96x96.png differ
diff --git a/www/assets/logo-dark.svg b/www/assets/logo-dark.svg
new file mode 100644
index 00000000000..09172c9cfd0
--- /dev/null
+++ b/www/assets/logo-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/www/assets/logo-light.svg b/www/assets/logo-light.svg
new file mode 100644
index 00000000000..b69582e4f3c
--- /dev/null
+++ b/www/assets/logo-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/www/favicon.ico b/www/favicon.ico
new file mode 100644
index 00000000000..997406ad22c
Binary files /dev/null and b/www/favicon.ico differ
diff --git a/www/index.html b/www/index.html
new file mode 100644
index 00000000000..74785f02749
--- /dev/null
+++ b/www/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+ Driver Test Harness | PlaceOS
+
+
+
+
+
+
+
+
+
+
+ Please enable JavaScript to continue using this application.
+
+
+
diff --git a/www/main.4f0c125d57b6a4294ef7.js b/www/main.4f0c125d57b6a4294ef7.js
new file mode 100644
index 00000000000..00ffdebb394
--- /dev/null
+++ b/www/main.4f0c125d57b6a4294ef7.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[1],{"/POA":function(e,t,i){window,e.exports=function(e){var t={};function i(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(n,r,(function(t){return e[t]}).bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=34)}([function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.forwardEvent=t.EventEmitter=void 0;var n=function(){function e(){this._listeners=[],this._disposed=!1}return Object.defineProperty(e.prototype,"event",{get:function(){var e=this;return this._event||(this._event=function(t){return e._listeners.push(t),{dispose:function(){if(!e._disposed)for(var i=0;i>22},t.prototype.getChars=function(){return 2097152&this.content?this.combinedData:2097151&this.content?s.stringFromCodePoint(2097151&this.content):""},t.prototype.getCode=function(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):2097151&this.content},t.prototype.setFromCharData=function(e){this.fg=e[o.CHAR_DATA_ATTR_INDEX],this.bg=0;var t=!1;if(e[o.CHAR_DATA_CHAR_INDEX].length>2)t=!0;else if(2===e[o.CHAR_DATA_CHAR_INDEX].length){var i=e[o.CHAR_DATA_CHAR_INDEX].charCodeAt(0);if(55296<=i&&i<=56319){var n=e[o.CHAR_DATA_CHAR_INDEX].charCodeAt(1);56320<=n&&n<=57343?this.content=1024*(i-55296)+n-56320+65536|e[o.CHAR_DATA_WIDTH_INDEX]<<22:t=!0}else t=!0}else this.content=e[o.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|e[o.CHAR_DATA_WIDTH_INDEX]<<22;t&&(this.combinedData=e[o.CHAR_DATA_CHAR_INDEX],this.content=2097152|e[o.CHAR_DATA_WIDTH_INDEX]<<22)},t.prototype.getAsCharData=function(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]},t}(a.AttributeData);t.CellData=l},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ISoundService=t.ISelectionService=t.IRenderService=t.IMouseService=t.ICoreBrowserService=t.ICharSizeService=void 0;var n=i(14);t.ICharSizeService=n.createDecorator("CharSizeService"),t.ICoreBrowserService=n.createDecorator("CoreBrowserService"),t.IMouseService=n.createDecorator("MouseService"),t.IRenderService=n.createDecorator("RenderService"),t.ISelectionService=n.createDecorator("SelectionService"),t.ISoundService=n.createDecorator("SoundService")},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ExtendedAttrs=t.AttributeData=void 0;var n=function(){function e(){this.fg=0,this.bg=0,this.extended=new r}return e.toColorRGB=function(e){return[e>>>16&255,e>>>8&255,255&e]},e.fromColorRGB=function(e){return(255&e[0])<<16|(255&e[1])<<8|255&e[2]},e.prototype.clone=function(){var t=new e;return t.fg=this.fg,t.bg=this.bg,t.extended=this.extended.clone(),t},e.prototype.isInverse=function(){return 67108864&this.fg},e.prototype.isBold=function(){return 134217728&this.fg},e.prototype.isUnderline=function(){return 268435456&this.fg},e.prototype.isBlink=function(){return 536870912&this.fg},e.prototype.isInvisible=function(){return 1073741824&this.fg},e.prototype.isItalic=function(){return 67108864&this.bg},e.prototype.isDim=function(){return 134217728&this.bg},e.prototype.getFgColorMode=function(){return 50331648&this.fg},e.prototype.getBgColorMode=function(){return 50331648&this.bg},e.prototype.isFgRGB=function(){return 50331648==(50331648&this.fg)},e.prototype.isBgRGB=function(){return 50331648==(50331648&this.bg)},e.prototype.isFgPalette=function(){return 16777216==(50331648&this.fg)||33554432==(50331648&this.fg)},e.prototype.isBgPalette=function(){return 16777216==(50331648&this.bg)||33554432==(50331648&this.bg)},e.prototype.isFgDefault=function(){return 0==(50331648&this.fg)},e.prototype.isBgDefault=function(){return 0==(50331648&this.bg)},e.prototype.isAttributeDefault=function(){return 0===this.fg&&0===this.bg},e.prototype.getFgColor=function(){switch(50331648&this.fg){case 16777216:case 33554432:return 255&this.fg;case 50331648:return 16777215&this.fg;default:return-1}},e.prototype.getBgColor=function(){switch(50331648&this.bg){case 16777216:case 33554432:return 255&this.bg;case 50331648:return 16777215&this.bg;default:return-1}},e.prototype.hasExtendedAttrs=function(){return 268435456&this.bg},e.prototype.updateExtended=function(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456},e.prototype.getUnderlineColor=function(){if(268435456&this.bg&&~this.extended.underlineColor)switch(50331648&this.extended.underlineColor){case 16777216:case 33554432:return 255&this.extended.underlineColor;case 50331648:return 16777215&this.extended.underlineColor;default:return this.getFgColor()}return this.getFgColor()},e.prototype.getUnderlineColorMode=function(){return 268435456&this.bg&&~this.extended.underlineColor?50331648&this.extended.underlineColor:this.getFgColorMode()},e.prototype.isUnderlineColorRGB=function(){return 268435456&this.bg&&~this.extended.underlineColor?50331648==(50331648&this.extended.underlineColor):this.isFgRGB()},e.prototype.isUnderlineColorPalette=function(){return 268435456&this.bg&&~this.extended.underlineColor?16777216==(50331648&this.extended.underlineColor)||33554432==(50331648&this.extended.underlineColor):this.isFgPalette()},e.prototype.isUnderlineColorDefault=function(){return 268435456&this.bg&&~this.extended.underlineColor?0==(50331648&this.extended.underlineColor):this.isFgDefault()},e.prototype.getUnderlineStyle=function(){return 268435456&this.fg?268435456&this.bg?this.extended.underlineStyle:1:0},e}();t.AttributeData=n;var r=function(){function e(e,t){void 0===e&&(e=0),void 0===t&&(t=-1),this.underlineStyle=e,this.underlineColor=t}return e.prototype.clone=function(){return new e(this.underlineStyle,this.underlineColor)},e.prototype.isEmpty=function(){return 0===this.underlineStyle},e}();t.ExtendedAttrs=r},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.addDisposableDomListener=void 0,t.addDisposableDomListener=function(e,t,i,n){e.addEventListener(t,i,n);var r=!1;return{dispose:function(){r||(r=!0,e.removeEventListener(t,i,n))}}}},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Utf8ToUtf32=t.StringToUtf32=t.utf32ToString=t.stringFromCodePoint=void 0,t.stringFromCodePoint=function(e){return e>65535?(e-=65536,String.fromCharCode(55296+(e>>10))+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)},t.utf32ToString=function(e,t,i){void 0===t&&(t=0),void 0===i&&(i=e.length);for(var n="",r=t;r65535?(s-=65536,n+=String.fromCharCode(55296+(s>>10))+String.fromCharCode(s%1024+56320)):n+=String.fromCharCode(s)}return n};var n=function(){function e(){this._interim=0}return e.prototype.clear=function(){this._interim=0},e.prototype.decode=function(e,t){var i=e.length;if(!i)return 0;var n=0,r=0;this._interim&&(56320<=(a=e.charCodeAt(r++))&&a<=57343?t[n++]=1024*(this._interim-55296)+a-56320+65536:(t[n++]=this._interim,t[n++]=a),this._interim=0);for(var s=r;s=i)return this._interim=o,n;var a;56320<=(a=e.charCodeAt(s))&&a<=57343?t[n++]=1024*(o-55296)+a-56320+65536:(t[n++]=o,t[n++]=a)}else t[n++]=o}return n},e}();t.StringToUtf32=n;var r=function(){function e(){this.interim=new Uint8Array(3)}return e.prototype.clear=function(){this.interim.fill(0)},e.prototype.decode=function(e,t){var i=e.length;if(!i)return 0;var n,r,s,o,a=0,l=0,c=0;if(this.interim[0]){var h=!1,u=this.interim[0];u&=192==(224&u)?31:224==(240&u)?15:7;for(var d=0,f=void 0;(f=63&this.interim[++d])&&d<4;)u<<=6,u|=f;for(var p=192==(224&this.interim[0])?2:224==(240&this.interim[0])?3:4,_=p-d;c<_;){if(c>=i)return 0;if(128!=(192&(f=e[c++]))){c--,h=!0;break}this.interim[d++]=f,u<<=6,u|=63&f}h||(2===p?u<128?c--:t[a++]=u:3===p?u<2048||u>=55296&&u<=57343||(t[a++]=u):u<65536||u>1114111||(t[a++]=u)),this.interim.fill(0)}for(var m=i-4,g=c;g=i)return this.interim[0]=n,a;if(128!=(192&(r=e[g++]))){g--;continue}if((l=(31&n)<<6|63&r)<128){g--;continue}t[a++]=l}else if(224==(240&n)){if(g>=i)return this.interim[0]=n,a;if(128!=(192&(r=e[g++]))){g--;continue}if(g>=i)return this.interim[0]=n,this.interim[1]=r,a;if(128!=(192&(s=e[g++]))){g--;continue}if((l=(15&n)<<12|(63&r)<<6|63&s)<2048||l>=55296&&l<=57343)continue;t[a++]=l}else if(240==(248&n)){if(g>=i)return this.interim[0]=n,a;if(128!=(192&(r=e[g++]))){g--;continue}if(g>=i)return this.interim[0]=n,this.interim[1]=r,a;if(128!=(192&(s=e[g++]))){g--;continue}if(g>=i)return this.interim[0]=n,this.interim[1]=r,this.interim[2]=s,a;if(128!=(192&(o=e[g++]))){g--;continue}if((l=(7&n)<<18|(63&r)<<12|(63&s)<<6|63&o)<65536||l>1114111)continue;t[a++]=l}}return a},e}();t.Utf8ToUtf32=r},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CHAR_ATLAS_CELL_SPACING=t.DIM_OPACITY=t.INVERTED_DEFAULT_COLOR=void 0,t.INVERTED_DEFAULT_COLOR=257,t.DIM_OPACITY=.5,t.CHAR_ATLAS_CELL_SPACING=1},function(e,t,i){"use strict";var n,r,s,o;function a(e){var t=e.toString(16);return t.length<2?"0"+t:t}function l(e,t){return e>>0}}(n=t.channels||(t.channels={})),(r=t.color||(t.color={})).blend=function(e,t){var i=(255&t.rgba)/255;if(1===i)return{css:t.css,rgba:t.rgba};var r=t.rgba>>16&255,s=t.rgba>>8&255,o=e.rgba>>24&255,a=e.rgba>>16&255,l=e.rgba>>8&255,c=o+Math.round(((t.rgba>>24&255)-o)*i),h=a+Math.round((r-a)*i),u=l+Math.round((s-l)*i);return{css:n.toCss(c,h,u),rgba:n.toRgba(c,h,u)}},r.isOpaque=function(e){return 255==(255&e.rgba)},r.ensureContrastRatio=function(e,t,i){var n=o.ensureContrastRatio(e.rgba,t.rgba,i);if(n)return o.toColor(n>>24&255,n>>16&255,n>>8&255)},r.opaque=function(e){var t=(255|e.rgba)>>>0,i=o.toChannels(t);return{css:n.toCss(i[0],i[1],i[2]),rgba:t}},r.opacity=function(e,t){var i=Math.round(255*t),r=o.toChannels(e.rgba),s=r[0],a=r[1],l=r[2];return{css:n.toCss(s,a,l,i),rgba:n.toRgba(s,a,l,i)}},(t.css||(t.css={})).toColor=function(e){switch(e.length){case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}throw new Error("css.toColor: Unsupported css format")},function(e){function t(e,t,i){var n=e/255,r=t/255,s=i/255;return.2126*(n<=.03928?n/12.92:Math.pow((n+.055)/1.055,2.4))+.7152*(r<=.03928?r/12.92:Math.pow((r+.055)/1.055,2.4))+.0722*(s<=.03928?s/12.92:Math.pow((s+.055)/1.055,2.4))}e.relativeLuminance=function(e){return t(e>>16&255,e>>8&255,255&e)},e.relativeLuminance2=t}(s=t.rgb||(t.rgb={})),function(e){function t(e,t,i){for(var n=e>>24&255,r=e>>16&255,o=e>>8&255,a=t>>24&255,c=t>>16&255,h=t>>8&255,u=l(s.relativeLuminance2(a,h,c),s.relativeLuminance2(n,r,o));u0||c>0||h>0);)a-=Math.max(0,Math.ceil(.1*a)),c-=Math.max(0,Math.ceil(.1*c)),h-=Math.max(0,Math.ceil(.1*h)),u=l(s.relativeLuminance2(a,h,c),s.relativeLuminance2(n,r,o));return(a<<24|c<<16|h<<8|255)>>>0}function i(e,t,i){for(var n=e>>24&255,r=e>>16&255,o=e>>8&255,a=t>>24&255,c=t>>16&255,h=t>>8&255,u=l(s.relativeLuminance2(a,h,c),s.relativeLuminance2(n,r,o));u>>0}e.ensureContrastRatio=function(e,n,r){var o=s.relativeLuminance(e>>8),a=s.relativeLuminance(n>>8);if(l(o,a)>24&255,e>>16&255,e>>8&255,255&e]},e.toColor=function(e,t,i){return{css:n.toCss(e,t,i),rgba:n.toRgba(e,t,i)}}}(o=t.rgba||(t.rgba={})),t.toPaddedHex=a,t.contrastRatio=l},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isLinux=t.isWindows=t.isIphone=t.isIpad=t.isMac=t.isSafari=t.isFirefox=void 0;var n="undefined"==typeof navigator,r=n?"node":navigator.userAgent,s=n?"node":navigator.platform;function o(e,t){return e.indexOf(t)>=0}t.isFirefox=!!~r.indexOf("Firefox"),t.isSafari=/^((?!chrome|android).)*safari/i.test(r),t.isMac=o(["Macintosh","MacIntel","MacPPC","Mac68K"],s),t.isIpad="iPad"===s,t.isIphone="iPhone"===s,t.isWindows=o(["Windows","Win16","Win32","WinCE"],s),t.isLinux=s.indexOf("Linux")>=0},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.C1=t.C0=void 0,function(e){e.NUL="\0",e.SOH="\x01",e.STX="\x02",e.ETX="\x03",e.EOT="\x04",e.ENQ="\x05",e.ACK="\x06",e.BEL="\x07",e.BS="\b",e.HT="\t",e.LF="\n",e.VT="\v",e.FF="\f",e.CR="\r",e.SO="\x0e",e.SI="\x0f",e.DLE="\x10",e.DC1="\x11",e.DC2="\x12",e.DC3="\x13",e.DC4="\x14",e.NAK="\x15",e.SYN="\x16",e.ETB="\x17",e.CAN="\x18",e.EM="\x19",e.SUB="\x1a",e.ESC="\x1b",e.FS="\x1c",e.GS="\x1d",e.RS="\x1e",e.US="\x1f",e.SP=" ",e.DEL="\x7f"}(t.C0||(t.C0={})),function(e){e.PAD="\x80",e.HOP="\x81",e.BPH="\x82",e.NBH="\x83",e.IND="\x84",e.NEL="\x85",e.SSA="\x86",e.ESA="\x87",e.HTS="\x88",e.HTJ="\x89",e.VTS="\x8a",e.PLD="\x8b",e.PLU="\x8c",e.RI="\x8d",e.SS2="\x8e",e.SS3="\x8f",e.DCS="\x90",e.PU1="\x91",e.PU2="\x92",e.STS="\x93",e.CCH="\x94",e.MW="\x95",e.SPA="\x96",e.EPA="\x97",e.SOS="\x98",e.SGCI="\x99",e.SCI="\x9a",e.CSI="\x9b",e.ST="\x9c",e.OSC="\x9d",e.PM="\x9e",e.APC="\x9f"}(t.C1||(t.C1={}))},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.BaseRenderLayer=void 0;var n=i(3),r=i(9),s=i(25),o=i(6),a=i(28),l=i(10),c=i(17),h=function(){function e(e,t,i,n,r,s,o,a){this._container=e,this._alpha=n,this._colors=r,this._rendererId=s,this._bufferService=o,this._optionsService=a,this._scaledCharWidth=0,this._scaledCharHeight=0,this._scaledCellWidth=0,this._scaledCellHeight=0,this._scaledCharLeft=0,this._scaledCharTop=0,this._currentGlyphIdentifier={chars:"",code:0,bg:0,fg:0,bold:!1,dim:!1,italic:!1},this._canvas=document.createElement("canvas"),this._canvas.classList.add("xterm-"+t+"-layer"),this._canvas.style.zIndex=i.toString(),this._initCanvas(),this._container.appendChild(this._canvas)}return e.prototype.dispose=function(){var e;c.removeElementFromParent(this._canvas),null===(e=this._charAtlas)||void 0===e||e.dispose()},e.prototype._initCanvas=function(){this._ctx=a.throwIfFalsy(this._canvas.getContext("2d",{alpha:this._alpha})),this._alpha||this._clearAll()},e.prototype.onOptionsChanged=function(){},e.prototype.onBlur=function(){},e.prototype.onFocus=function(){},e.prototype.onCursorMove=function(){},e.prototype.onGridChanged=function(e,t){},e.prototype.onSelectionChanged=function(e,t,i){void 0===i&&(i=!1)},e.prototype.setColors=function(e){this._refreshCharAtlas(e)},e.prototype._setTransparency=function(e){if(e!==this._alpha){var t=this._canvas;this._alpha=e,this._canvas=this._canvas.cloneNode(),this._initCanvas(),this._container.replaceChild(this._canvas,t),this._refreshCharAtlas(this._colors),this.onGridChanged(0,this._bufferService.rows-1)}},e.prototype._refreshCharAtlas=function(e){this._scaledCharWidth<=0&&this._scaledCharHeight<=0||(this._charAtlas=s.acquireCharAtlas(this._optionsService.options,this._rendererId,e,this._scaledCharWidth,this._scaledCharHeight),this._charAtlas.warmUp())},e.prototype.resize=function(e){this._scaledCellWidth=e.scaledCellWidth,this._scaledCellHeight=e.scaledCellHeight,this._scaledCharWidth=e.scaledCharWidth,this._scaledCharHeight=e.scaledCharHeight,this._scaledCharLeft=e.scaledCharLeft,this._scaledCharTop=e.scaledCharTop,this._canvas.width=e.scaledCanvasWidth,this._canvas.height=e.scaledCanvasHeight,this._canvas.style.width=e.canvasWidth+"px",this._canvas.style.height=e.canvasHeight+"px",this._alpha||this._clearAll(),this._refreshCharAtlas(this._colors)},e.prototype._fillCells=function(e,t,i,n){this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,i*this._scaledCellWidth,n*this._scaledCellHeight)},e.prototype._fillBottomLineAtCells=function(e,t,i){void 0===i&&(i=1),this._ctx.fillRect(e*this._scaledCellWidth,(t+1)*this._scaledCellHeight-window.devicePixelRatio-1,i*this._scaledCellWidth,window.devicePixelRatio)},e.prototype._fillLeftLineAtCell=function(e,t,i){this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,window.devicePixelRatio*i,this._scaledCellHeight)},e.prototype._strokeRectAtCell=function(e,t,i,n){this._ctx.lineWidth=window.devicePixelRatio,this._ctx.strokeRect(e*this._scaledCellWidth+window.devicePixelRatio/2,t*this._scaledCellHeight+window.devicePixelRatio/2,i*this._scaledCellWidth-window.devicePixelRatio,n*this._scaledCellHeight-window.devicePixelRatio)},e.prototype._clearAll=function(){this._alpha?this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height):(this._ctx.fillStyle=this._colors.background.css,this._ctx.fillRect(0,0,this._canvas.width,this._canvas.height))},e.prototype._clearCells=function(e,t,i,n){this._alpha?this._ctx.clearRect(e*this._scaledCellWidth,t*this._scaledCellHeight,i*this._scaledCellWidth,n*this._scaledCellHeight):(this._ctx.fillStyle=this._colors.background.css,this._ctx.fillRect(e*this._scaledCellWidth,t*this._scaledCellHeight,i*this._scaledCellWidth,n*this._scaledCellHeight))},e.prototype._fillCharTrueColor=function(e,t,i){this._ctx.font=this._getFont(!1,!1),this._ctx.textBaseline="middle",this._clipRow(i),this._ctx.fillText(e.getChars(),t*this._scaledCellWidth+this._scaledCharLeft,i*this._scaledCellHeight+this._scaledCharTop+this._scaledCharHeight/2)},e.prototype._drawChars=function(e,t,i){var s,o,a=this._getContrastColor(e);a||e.isFgRGB()||e.isBgRGB()?this._drawUncachedChars(e,t,i,a):(e.isInverse()?(s=e.isBgDefault()?r.INVERTED_DEFAULT_COLOR:e.getBgColor(),o=e.isFgDefault()?r.INVERTED_DEFAULT_COLOR:e.getFgColor()):(o=e.isBgDefault()?n.DEFAULT_COLOR:e.getBgColor(),s=e.isFgDefault()?n.DEFAULT_COLOR:e.getFgColor()),s+=this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&s<8?8:0,this._currentGlyphIdentifier.chars=e.getChars()||n.WHITESPACE_CELL_CHAR,this._currentGlyphIdentifier.code=e.getCode()||n.WHITESPACE_CELL_CODE,this._currentGlyphIdentifier.bg=o,this._currentGlyphIdentifier.fg=s,this._currentGlyphIdentifier.bold=!!e.isBold(),this._currentGlyphIdentifier.dim=!!e.isDim(),this._currentGlyphIdentifier.italic=!!e.isItalic(),this._charAtlas&&this._charAtlas.draw(this._ctx,this._currentGlyphIdentifier,t*this._scaledCellWidth+this._scaledCharLeft,i*this._scaledCellHeight+this._scaledCharTop)||this._drawUncachedChars(e,t,i))},e.prototype._drawUncachedChars=function(e,t,i,n){if(this._ctx.save(),this._ctx.font=this._getFont(!!e.isBold(),!!e.isItalic()),this._ctx.textBaseline="middle",e.isInverse())if(n)this._ctx.fillStyle=n.css;else if(e.isBgDefault())this._ctx.fillStyle=l.color.opaque(this._colors.background).css;else if(e.isBgRGB())this._ctx.fillStyle="rgb("+o.AttributeData.toColorRGB(e.getBgColor()).join(",")+")";else{var s=e.getBgColor();this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&s<8&&(s+=8),this._ctx.fillStyle=this._colors.ansi[s].css}else if(n)this._ctx.fillStyle=n.css;else if(e.isFgDefault())this._ctx.fillStyle=this._colors.foreground.css;else if(e.isFgRGB())this._ctx.fillStyle="rgb("+o.AttributeData.toColorRGB(e.getFgColor()).join(",")+")";else{var a=e.getFgColor();this._optionsService.options.drawBoldTextInBrightColors&&e.isBold()&&a<8&&(a+=8),this._ctx.fillStyle=this._colors.ansi[a].css}this._clipRow(i),e.isDim()&&(this._ctx.globalAlpha=r.DIM_OPACITY),this._ctx.fillText(e.getChars(),t*this._scaledCellWidth+this._scaledCharLeft,i*this._scaledCellHeight+this._scaledCharTop+this._scaledCharHeight/2),this._ctx.restore()},e.prototype._clipRow=function(e){this._ctx.beginPath(),this._ctx.rect(0,e*this._scaledCellHeight,this._bufferService.cols*this._scaledCellWidth,this._scaledCellHeight),this._ctx.clip()},e.prototype._getFont=function(e,t){return(t?"italic":"")+" "+(e?this._optionsService.options.fontWeightBold:this._optionsService.options.fontWeight)+" "+this._optionsService.options.fontSize*window.devicePixelRatio+"px "+this._optionsService.options.fontFamily},e.prototype._getContrastColor=function(e){if(1!==this._optionsService.options.minimumContrastRatio){var t=this._colors.contrastCache.getColor(e.bg,e.fg);if(void 0!==t)return t||void 0;var i=e.getFgColor(),n=e.getFgColorMode(),r=e.getBgColor(),s=e.getBgColorMode(),o=!!e.isInverse(),a=!!e.isInverse();if(o){var c=i;i=r,r=c;var h=n;n=s,s=h}var u=this._resolveBackgroundRgba(s,r,o),d=this._resolveForegroundRgba(n,i,o,a),f=l.rgba.ensureContrastRatio(u,d,this._optionsService.options.minimumContrastRatio);if(f){var p={css:l.channels.toCss(f>>24&255,f>>16&255,f>>8&255),rgba:f};return this._colors.contrastCache.setColor(e.bg,e.fg,p),p}this._colors.contrastCache.setColor(e.bg,e.fg,null)}},e.prototype._resolveBackgroundRgba=function(e,t,i){switch(e){case 16777216:case 33554432:return this._colors.ansi[t].rgba;case 50331648:return t<<8;case 0:default:return i?this._colors.foreground.rgba:this._colors.background.rgba}},e.prototype._resolveForegroundRgba=function(e,t,i,n){switch(e){case 16777216:case 33554432:return this._optionsService.options.drawBoldTextInBrightColors&&n&&t<8&&(t+=8),this._colors.ansi[t].rgba;case 50331648:return t<<8;case 0:default:return i?this._colors.background.rgba:this._colors.foreground.rgba}},e}();t.BaseRenderLayer=h},function(e,t,i){"use strict";function n(e,t,i){t.di$target===t?t.di$dependencies.push({id:e,index:i}):(t.di$dependencies=[{id:e,index:i}],t.di$target=t)}Object.defineProperty(t,"__esModule",{value:!0}),t.createDecorator=t.getServiceDependencies=t.serviceRegistry=void 0,t.serviceRegistry=new Map,t.getServiceDependencies=function(e){return e.di$dependencies||[]},t.createDecorator=function(e){if(t.serviceRegistry.has(e))return t.serviceRegistry.get(e);var i=function(e,t,r){if(3!==arguments.length)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");n(i,e,r)};return i.toString=function(){return e},t.serviceRegistry.set(e,i),i}},function(e,t,i){"use strict";function n(e,t,i,n){if(void 0===i&&(i=0),void 0===n&&(n=e.length),i>=e.length)return e;n=n>=e.length?e.length:(e.length+n)%e.length;for(var r=i=(e.length+i)%e.length;r>22,2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):i]},e.prototype.set=function(e,t){this._data[3*e+1]=t[r.CHAR_DATA_ATTR_INDEX],t[r.CHAR_DATA_CHAR_INDEX].length>1?(this._combined[e]=t[1],this._data[3*e+0]=2097152|e|t[r.CHAR_DATA_WIDTH_INDEX]<<22):this._data[3*e+0]=t[r.CHAR_DATA_CHAR_INDEX].charCodeAt(0)|t[r.CHAR_DATA_WIDTH_INDEX]<<22},e.prototype.getWidth=function(e){return this._data[3*e+0]>>22},e.prototype.hasWidth=function(e){return 12582912&this._data[3*e+0]},e.prototype.getFg=function(e){return this._data[3*e+1]},e.prototype.getBg=function(e){return this._data[3*e+2]},e.prototype.hasContent=function(e){return 4194303&this._data[3*e+0]},e.prototype.getCodePoint=function(e){var t=this._data[3*e+0];return 2097152&t?this._combined[e].charCodeAt(this._combined[e].length-1):2097151&t},e.prototype.isCombined=function(e){return 2097152&this._data[3*e+0]},e.prototype.getString=function(e){var t=this._data[3*e+0];return 2097152&t?this._combined[e]:2097151&t?n.stringFromCodePoint(2097151&t):""},e.prototype.loadCell=function(e,t){var i=3*e;return t.content=this._data[i+0],t.fg=this._data[i+1],t.bg=this._data[i+2],2097152&t.content&&(t.combinedData=this._combined[e]),268435456&t.bg&&(t.extended=this._extendedAttrs[e]),t},e.prototype.setCell=function(e,t){2097152&t.content&&(this._combined[e]=t.combinedData),268435456&t.bg&&(this._extendedAttrs[e]=t.extended),this._data[3*e+0]=t.content,this._data[3*e+1]=t.fg,this._data[3*e+2]=t.bg},e.prototype.setCellFromCodePoint=function(e,t,i,n,r,s){268435456&r&&(this._extendedAttrs[e]=s),this._data[3*e+0]=t|i<<22,this._data[3*e+1]=n,this._data[3*e+2]=r},e.prototype.addCodepointToCell=function(e,t){var i=this._data[3*e+0];2097152&i?this._combined[e]+=n.stringFromCodePoint(t):(2097151&i?(this._combined[e]=n.stringFromCodePoint(2097151&i)+n.stringFromCodePoint(t),i&=-2097152,i|=2097152):i=t|1<<22,this._data[3*e+0]=i)},e.prototype.insertCells=function(e,t,i,n){if((e%=this.length)&&2===this.getWidth(e-1)&&this.setCellFromCodePoint(e-1,0,1,(null==n?void 0:n.fg)||0,(null==n?void 0:n.bg)||0,(null==n?void 0:n.extended)||new o.ExtendedAttrs),t=0;--a)this.setCell(e+t+a,this.loadCell(e+a,r));for(a=0;athis.length){var i=new Uint32Array(3*e);this.length&&i.set(3*e=e&&delete this._combined[s]}}else this._data=new Uint32Array(0),this._combined={};this.length=e}},e.prototype.fill=function(e){this._combined={},this._extendedAttrs={};for(var t=0;t=0;--e)if(4194303&this._data[3*e+0])return e+(this._data[3*e+0]>>22);return 0},e.prototype.copyCellsFrom=function(e,t,i,n,r){var s=e._data;if(r)for(var o=n-1;o>=0;o--)for(var a=0;a<3;a++)this._data[3*(i+o)+a]=s[3*(t+o)+a];else for(o=0;o=t&&(this._combined[c-t+i]=e._combined[c])}},e.prototype.translateToString=function(e,t,i){void 0===e&&(e=!1),void 0===t&&(t=0),void 0===i&&(i=this.length),e&&(i=Math.min(i,this.getTrimmedLength()));for(var s="";t>22||1}return s},e}();t.BufferLine=a},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.removeElementFromParent=void 0,t.removeElementFromParent=function(){for(var e,t=[],i=0;i24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}!function(e){e[e.GET_WIN_SIZE_PIXELS=0]="GET_WIN_SIZE_PIXELS",e[e.GET_CELL_SIZE_PIXELS=1]="GET_CELL_SIZE_PIXELS"}(s=t.WindowsOptionsReportType||(t.WindowsOptionsReportType={}));var C=function(){function e(e,t,i,n){this._bufferService=e,this._coreService=t,this._logService=i,this._optionsService=n,this._data=new Uint32Array(0)}return e.prototype.hook=function(e){this._data=new Uint32Array(0)},e.prototype.put=function(e,t,i){this._data=h.concat(this._data,e.subarray(t,i))},e.prototype.unhook=function(e){if(e){var t=u.utf32ToString(this._data);switch(this._data=new Uint32Array(0),t){case'"q':return this._coreService.triggerDataEvent(o.C0.ESC+'P1$r0"q'+o.C0.ESC+"\\");case'"p':return this._coreService.triggerDataEvent(o.C0.ESC+'P1$r61;1"p'+o.C0.ESC+"\\");case"r":return this._coreService.triggerDataEvent(o.C0.ESC+"P1$r"+(this._bufferService.buffer.scrollTop+1)+";"+(this._bufferService.buffer.scrollBottom+1)+"r"+o.C0.ESC+"\\");case"m":return this._coreService.triggerDataEvent(o.C0.ESC+"P1$r0m"+o.C0.ESC+"\\");case" q":var i={block:2,underline:4,bar:6}[this._optionsService.options.cursorStyle];return this._coreService.triggerDataEvent(o.C0.ESC+"P1$r"+(i-=this._optionsService.options.cursorBlink?1:0)+" q"+o.C0.ESC+"\\");default:this._logService.debug("Unknown DCS $q %s",t),this._coreService.triggerDataEvent(o.C0.ESC+"P0$r"+o.C0.ESC+"\\")}}else this._data=new Uint32Array(0)},e}(),w=function(e){function t(t,i,n,r,s,c,h,p,m){void 0===m&&(m=new l.EscapeSequenceParser);var v=e.call(this)||this;v._bufferService=t,v._charsetService=i,v._coreService=n,v._dirtyRowService=r,v._logService=s,v._optionsService=c,v._coreMouseService=h,v._unicodeService=p,v._parser=m,v._parseBuffer=new Uint32Array(4096),v._stringDecoder=new u.StringToUtf32,v._utf8Decoder=new u.Utf8ToUtf32,v._workCell=new _.CellData,v._windowTitle="",v._iconName="",v._windowTitleStack=[],v._iconNameStack=[],v._curAttrData=d.DEFAULT_ATTR_DATA.clone(),v._eraseAttrDataInternal=d.DEFAULT_ATTR_DATA.clone(),v._onRequestBell=new f.EventEmitter,v._onRequestRefreshRows=new f.EventEmitter,v._onRequestReset=new f.EventEmitter,v._onRequestScroll=new f.EventEmitter,v._onRequestSyncScrollBar=new f.EventEmitter,v._onRequestWindowsOptionsReport=new f.EventEmitter,v._onA11yChar=new f.EventEmitter,v._onA11yTab=new f.EventEmitter,v._onCursorMove=new f.EventEmitter,v._onLineFeed=new f.EventEmitter,v._onScroll=new f.EventEmitter,v._onTitleChange=new f.EventEmitter,v.register(v._parser),v._parser.setCsiHandlerFallback((function(e,t){v._logService.debug("Unknown CSI code: ",{identifier:v._parser.identToString(e),params:t.toArray()})})),v._parser.setEscHandlerFallback((function(e){v._logService.debug("Unknown ESC code: ",{identifier:v._parser.identToString(e)})})),v._parser.setExecuteHandlerFallback((function(e){v._logService.debug("Unknown EXECUTE code: ",{code:e})})),v._parser.setOscHandlerFallback((function(e,t,i){v._logService.debug("Unknown OSC code: ",{identifier:e,action:t,data:i})})),v._parser.setDcsHandlerFallback((function(e,t,i){"HOOK"===t&&(i=i.toArray()),v._logService.debug("Unknown DCS code: ",{identifier:v._parser.identToString(e),action:t,payload:i})})),v._parser.setPrintHandler((function(e,t,i){return v.print(e,t,i)})),v._parser.setCsiHandler({final:"@"},(function(e){return v.insertChars(e)})),v._parser.setCsiHandler({intermediates:" ",final:"@"},(function(e){return v.scrollLeft(e)})),v._parser.setCsiHandler({final:"A"},(function(e){return v.cursorUp(e)})),v._parser.setCsiHandler({intermediates:" ",final:"A"},(function(e){return v.scrollRight(e)})),v._parser.setCsiHandler({final:"B"},(function(e){return v.cursorDown(e)})),v._parser.setCsiHandler({final:"C"},(function(e){return v.cursorForward(e)})),v._parser.setCsiHandler({final:"D"},(function(e){return v.cursorBackward(e)})),v._parser.setCsiHandler({final:"E"},(function(e){return v.cursorNextLine(e)})),v._parser.setCsiHandler({final:"F"},(function(e){return v.cursorPrecedingLine(e)})),v._parser.setCsiHandler({final:"G"},(function(e){return v.cursorCharAbsolute(e)})),v._parser.setCsiHandler({final:"H"},(function(e){return v.cursorPosition(e)})),v._parser.setCsiHandler({final:"I"},(function(e){return v.cursorForwardTab(e)})),v._parser.setCsiHandler({final:"J"},(function(e){return v.eraseInDisplay(e)})),v._parser.setCsiHandler({prefix:"?",final:"J"},(function(e){return v.eraseInDisplay(e)})),v._parser.setCsiHandler({final:"K"},(function(e){return v.eraseInLine(e)})),v._parser.setCsiHandler({prefix:"?",final:"K"},(function(e){return v.eraseInLine(e)})),v._parser.setCsiHandler({final:"L"},(function(e){return v.insertLines(e)})),v._parser.setCsiHandler({final:"M"},(function(e){return v.deleteLines(e)})),v._parser.setCsiHandler({final:"P"},(function(e){return v.deleteChars(e)})),v._parser.setCsiHandler({final:"S"},(function(e){return v.scrollUp(e)})),v._parser.setCsiHandler({final:"T"},(function(e){return v.scrollDown(e)})),v._parser.setCsiHandler({final:"X"},(function(e){return v.eraseChars(e)})),v._parser.setCsiHandler({final:"Z"},(function(e){return v.cursorBackwardTab(e)})),v._parser.setCsiHandler({final:"`"},(function(e){return v.charPosAbsolute(e)})),v._parser.setCsiHandler({final:"a"},(function(e){return v.hPositionRelative(e)})),v._parser.setCsiHandler({final:"b"},(function(e){return v.repeatPrecedingCharacter(e)})),v._parser.setCsiHandler({final:"c"},(function(e){return v.sendDeviceAttributesPrimary(e)})),v._parser.setCsiHandler({prefix:">",final:"c"},(function(e){return v.sendDeviceAttributesSecondary(e)})),v._parser.setCsiHandler({final:"d"},(function(e){return v.linePosAbsolute(e)})),v._parser.setCsiHandler({final:"e"},(function(e){return v.vPositionRelative(e)})),v._parser.setCsiHandler({final:"f"},(function(e){return v.hVPosition(e)})),v._parser.setCsiHandler({final:"g"},(function(e){return v.tabClear(e)})),v._parser.setCsiHandler({final:"h"},(function(e){return v.setMode(e)})),v._parser.setCsiHandler({prefix:"?",final:"h"},(function(e){return v.setModePrivate(e)})),v._parser.setCsiHandler({final:"l"},(function(e){return v.resetMode(e)})),v._parser.setCsiHandler({prefix:"?",final:"l"},(function(e){return v.resetModePrivate(e)})),v._parser.setCsiHandler({final:"m"},(function(e){return v.charAttributes(e)})),v._parser.setCsiHandler({final:"n"},(function(e){return v.deviceStatus(e)})),v._parser.setCsiHandler({prefix:"?",final:"n"},(function(e){return v.deviceStatusPrivate(e)})),v._parser.setCsiHandler({intermediates:"!",final:"p"},(function(e){return v.softReset(e)})),v._parser.setCsiHandler({intermediates:" ",final:"q"},(function(e){return v.setCursorStyle(e)})),v._parser.setCsiHandler({final:"r"},(function(e){return v.setScrollRegion(e)})),v._parser.setCsiHandler({final:"s"},(function(e){return v.saveCursor(e)})),v._parser.setCsiHandler({final:"t"},(function(e){return v.windowOptions(e)})),v._parser.setCsiHandler({final:"u"},(function(e){return v.restoreCursor(e)})),v._parser.setCsiHandler({intermediates:"'",final:"}"},(function(e){return v.insertColumns(e)})),v._parser.setCsiHandler({intermediates:"'",final:"~"},(function(e){return v.deleteColumns(e)})),v._parser.setExecuteHandler(o.C0.BEL,(function(){return v.bell()})),v._parser.setExecuteHandler(o.C0.LF,(function(){return v.lineFeed()})),v._parser.setExecuteHandler(o.C0.VT,(function(){return v.lineFeed()})),v._parser.setExecuteHandler(o.C0.FF,(function(){return v.lineFeed()})),v._parser.setExecuteHandler(o.C0.CR,(function(){return v.carriageReturn()})),v._parser.setExecuteHandler(o.C0.BS,(function(){return v.backspace()})),v._parser.setExecuteHandler(o.C0.HT,(function(){return v.tab()})),v._parser.setExecuteHandler(o.C0.SO,(function(){return v.shiftOut()})),v._parser.setExecuteHandler(o.C0.SI,(function(){return v.shiftIn()})),v._parser.setExecuteHandler(o.C1.IND,(function(){return v.index()})),v._parser.setExecuteHandler(o.C1.NEL,(function(){return v.nextLine()})),v._parser.setExecuteHandler(o.C1.HTS,(function(){return v.tabSet()})),v._parser.setOscHandler(0,new g.OscHandler((function(e){v.setTitle(e),v.setIconName(e)}))),v._parser.setOscHandler(1,new g.OscHandler((function(e){return v.setIconName(e)}))),v._parser.setOscHandler(2,new g.OscHandler((function(e){return v.setTitle(e)}))),v._parser.setEscHandler({final:"7"},(function(){return v.saveCursor()})),v._parser.setEscHandler({final:"8"},(function(){return v.restoreCursor()})),v._parser.setEscHandler({final:"D"},(function(){return v.index()})),v._parser.setEscHandler({final:"E"},(function(){return v.nextLine()})),v._parser.setEscHandler({final:"H"},(function(){return v.tabSet()})),v._parser.setEscHandler({final:"M"},(function(){return v.reverseIndex()})),v._parser.setEscHandler({final:"="},(function(){return v.keypadApplicationMode()})),v._parser.setEscHandler({final:">"},(function(){return v.keypadNumericMode()})),v._parser.setEscHandler({final:"c"},(function(){return v.fullReset()})),v._parser.setEscHandler({final:"n"},(function(){return v.setgLevel(2)})),v._parser.setEscHandler({final:"o"},(function(){return v.setgLevel(3)})),v._parser.setEscHandler({final:"|"},(function(){return v.setgLevel(3)})),v._parser.setEscHandler({final:"}"},(function(){return v.setgLevel(2)})),v._parser.setEscHandler({final:"~"},(function(){return v.setgLevel(1)})),v._parser.setEscHandler({intermediates:"%",final:"@"},(function(){return v.selectDefaultCharset()})),v._parser.setEscHandler({intermediates:"%",final:"G"},(function(){return v.selectDefaultCharset()}));var y=function(e){b._parser.setEscHandler({intermediates:"(",final:e},(function(){return v.selectCharset("("+e)})),b._parser.setEscHandler({intermediates:")",final:e},(function(){return v.selectCharset(")"+e)})),b._parser.setEscHandler({intermediates:"*",final:e},(function(){return v.selectCharset("*"+e)})),b._parser.setEscHandler({intermediates:"+",final:e},(function(){return v.selectCharset("+"+e)})),b._parser.setEscHandler({intermediates:"-",final:e},(function(){return v.selectCharset("-"+e)})),b._parser.setEscHandler({intermediates:".",final:e},(function(){return v.selectCharset("."+e)})),b._parser.setEscHandler({intermediates:"/",final:e},(function(){return v.selectCharset("/"+e)}))},b=this;for(var w in a.CHARSETS)y(w);return v._parser.setEscHandler({intermediates:"#",final:"8"},(function(){return v.screenAlignmentPattern()})),v._parser.setErrorHandler((function(e){return v._logService.error("Parsing error: ",e),e})),v._parser.setDcsHandler({intermediates:"$",final:"q"},new C(v._bufferService,v._coreService,v._logService,v._optionsService)),v}return r(t,e),Object.defineProperty(t.prototype,"onRequestBell",{get:function(){return this._onRequestBell.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestRefreshRows",{get:function(){return this._onRequestRefreshRows.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestReset",{get:function(){return this._onRequestReset.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestScroll",{get:function(){return this._onRequestScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestSyncScrollBar",{get:function(){return this._onRequestSyncScrollBar.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestWindowsOptionsReport",{get:function(){return this._onRequestWindowsOptionsReport.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yChar",{get:function(){return this._onA11yChar.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yTab",{get:function(){return this._onA11yTab.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onCursorMove",{get:function(){return this._onCursorMove.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onLineFeed",{get:function(){return this._onLineFeed.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onScroll",{get:function(){return this._onScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onTitleChange",{get:function(){return this._onTitleChange.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){e.prototype.dispose.call(this)},t.prototype.parse=function(e){var t=this._bufferService.buffer,i=t.x,n=t.y;if(this._logService.debug("parsing data",e),this._parseBuffer.length131072)for(var r=0;r0&&2===f.getWidth(s.x-1)&&f.setCellFromCodePoint(s.x-1,0,1,d.fg,d.bg,d.extended);for(var _=t;_=l)if(c){for(;s.x=this._bufferService.rows&&(s.y=this._bufferService.rows-1),s.lines.get(s.ybase+s.y).isWrapped=!0),f=s.lines.get(s.ybase+s.y)}else if(s.x=l-1,2===r)continue;if(h&&(f.insertCells(s.x,r,s.getNullCell(d),d),2===f.getWidth(l-1)&&f.setCellFromCodePoint(l-1,p.NULL_CELL_CODE,p.NULL_CELL_WIDTH,d.fg,d.bg,d.extended)),f.setCellFromCodePoint(s.x++,n,r,d.fg,d.bg,d.extended),r>0)for(;--r;)f.setCellFromCodePoint(s.x++,0,0,d.fg,d.bg,d.extended)}else f.getWidth(s.x-1)?f.addCodepointToCell(s.x-1,n):f.addCodepointToCell(s.x-2,n)}i-t>0&&(f.loadCell(s.x-1,this._workCell),this._parser.precedingCodepoint=2===this._workCell.getWidth()||this._workCell.getCode()>65535?0:this._workCell.isCombined()?this._workCell.getChars().charCodeAt(0):this._workCell.content),s.x0&&0===f.getWidth(s.x)&&!f.hasContent(s.x)&&f.setCellFromCodePoint(s.x,0,1,d.fg,d.bg,d.extended),this._dirtyRowService.markDirty(s.y)},t.prototype.addCsiHandler=function(e,t){var i=this;return this._parser.addCsiHandler(e,"t"!==e.final||e.prefix||e.intermediates?t:function(e){return!b(e.params[0],i._optionsService.options.windowOptions)||t(e)})},t.prototype.addDcsHandler=function(e,t){return this._parser.addDcsHandler(e,new v.DcsHandler(t))},t.prototype.addEscHandler=function(e,t){return this._parser.addEscHandler(e,t)},t.prototype.addOscHandler=function(e,t){return this._parser.addOscHandler(e,new g.OscHandler(t))},t.prototype.bell=function(){this._onRequestBell.fire()},t.prototype.lineFeed=function(){var e=this._bufferService.buffer;this._dirtyRowService.markDirty(e.y),this._optionsService.options.convertEol&&(e.x=0),e.y++,e.y===e.scrollBottom+1?(e.y--,this._onRequestScroll.fire(this._eraseAttrData())):e.y>=this._bufferService.rows&&(e.y=this._bufferService.rows-1),e.x>=this._bufferService.cols&&e.x--,this._dirtyRowService.markDirty(e.y),this._onLineFeed.fire()},t.prototype.carriageReturn=function(){this._bufferService.buffer.x=0},t.prototype.backspace=function(){var e,t=this._bufferService.buffer;if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),void(t.x>0&&t.x--);if(this._restrictCursor(this._bufferService.cols),t.x>0)t.x--;else if(0===t.x&&t.y>t.scrollTop&&t.y<=t.scrollBottom&&(null===(e=t.lines.get(t.ybase+t.y))||void 0===e?void 0:e.isWrapped)){t.lines.get(t.ybase+t.y).isWrapped=!1,t.y--,t.x=this._bufferService.cols-1;var i=t.lines.get(t.ybase+t.y);i.hasWidth(t.x)&&!i.hasContent(t.x)&&t.x--}this._restrictCursor()},t.prototype.tab=function(){if(!(this._bufferService.buffer.x>=this._bufferService.cols)){var e=this._bufferService.buffer.x;this._bufferService.buffer.x=this._bufferService.buffer.nextStop(),this._optionsService.options.screenReaderMode&&this._onA11yTab.fire(this._bufferService.buffer.x-e)}},t.prototype.shiftOut=function(){this._charsetService.setgLevel(1)},t.prototype.shiftIn=function(){this._charsetService.setgLevel(0)},t.prototype._restrictCursor=function(e){void 0===e&&(e=this._bufferService.cols-1),this._bufferService.buffer.x=Math.min(e,Math.max(0,this._bufferService.buffer.x)),this._bufferService.buffer.y=this._coreService.decPrivateModes.origin?Math.min(this._bufferService.buffer.scrollBottom,Math.max(this._bufferService.buffer.scrollTop,this._bufferService.buffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._bufferService.buffer.y)),this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype._setCursor=function(e,t){this._dirtyRowService.markDirty(this._bufferService.buffer.y),this._coreService.decPrivateModes.origin?(this._bufferService.buffer.x=e,this._bufferService.buffer.y=this._bufferService.buffer.scrollTop+t):(this._bufferService.buffer.x=e,this._bufferService.buffer.y=t),this._restrictCursor(),this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype._moveCursor=function(e,t){this._restrictCursor(),this._setCursor(this._bufferService.buffer.x+e,this._bufferService.buffer.y+t)},t.prototype.cursorUp=function(e){var t=this._bufferService.buffer.y-this._bufferService.buffer.scrollTop;this._moveCursor(0,t>=0?-Math.min(t,e.params[0]||1):-(e.params[0]||1))},t.prototype.cursorDown=function(e){var t=this._bufferService.buffer.scrollBottom-this._bufferService.buffer.y;this._moveCursor(0,t>=0?Math.min(t,e.params[0]||1):e.params[0]||1)},t.prototype.cursorForward=function(e){this._moveCursor(e.params[0]||1,0)},t.prototype.cursorBackward=function(e){this._moveCursor(-(e.params[0]||1),0)},t.prototype.cursorNextLine=function(e){this.cursorDown(e),this._bufferService.buffer.x=0},t.prototype.cursorPrecedingLine=function(e){this.cursorUp(e),this._bufferService.buffer.x=0},t.prototype.cursorCharAbsolute=function(e){this._setCursor((e.params[0]||1)-1,this._bufferService.buffer.y)},t.prototype.cursorPosition=function(e){this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1)},t.prototype.charPosAbsolute=function(e){this._setCursor((e.params[0]||1)-1,this._bufferService.buffer.y)},t.prototype.hPositionRelative=function(e){this._moveCursor(e.params[0]||1,0)},t.prototype.linePosAbsolute=function(e){this._setCursor(this._bufferService.buffer.x,(e.params[0]||1)-1)},t.prototype.vPositionRelative=function(e){this._moveCursor(0,e.params[0]||1)},t.prototype.hVPosition=function(e){this.cursorPosition(e)},t.prototype.tabClear=function(e){var t=e.params[0];0===t?delete this._bufferService.buffer.tabs[this._bufferService.buffer.x]:3===t&&(this._bufferService.buffer.tabs={})},t.prototype.cursorForwardTab=function(e){if(!(this._bufferService.buffer.x>=this._bufferService.cols))for(var t=e.params[0]||1;t--;)this._bufferService.buffer.x=this._bufferService.buffer.nextStop()},t.prototype.cursorBackwardTab=function(e){if(!(this._bufferService.buffer.x>=this._bufferService.cols))for(var t=e.params[0]||1,i=this._bufferService.buffer;t--;)i.x=i.prevStop()},t.prototype._eraseInBufferLine=function(e,t,i,n){void 0===n&&(n=!1);var r=this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase+e);r.replaceCells(t,i,this._bufferService.buffer.getNullCell(this._eraseAttrData()),this._eraseAttrData()),n&&(r.isWrapped=!1)},t.prototype._resetBufferLine=function(e){var t=this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase+e);t.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())),t.isWrapped=!1},t.prototype.eraseInDisplay=function(e){var t;switch(this._restrictCursor(),e.params[0]){case 0:for(this._dirtyRowService.markDirty(t=this._bufferService.buffer.y),this._eraseInBufferLine(t++,this._bufferService.buffer.x,this._bufferService.cols,0===this._bufferService.buffer.x);t=this._bufferService.cols&&(this._bufferService.buffer.lines.get(t+1).isWrapped=!1);t--;)this._resetBufferLine(t);this._dirtyRowService.markDirty(0);break;case 2:for(this._dirtyRowService.markDirty((t=this._bufferService.rows)-1);t--;)this._resetBufferLine(t);this._dirtyRowService.markDirty(0);break;case 3:var i=this._bufferService.buffer.lines.length-this._bufferService.rows;i>0&&(this._bufferService.buffer.lines.trimStart(i),this._bufferService.buffer.ybase=Math.max(this._bufferService.buffer.ybase-i,0),this._bufferService.buffer.ydisp=Math.max(this._bufferService.buffer.ydisp-i,0),this._onScroll.fire(0))}},t.prototype.eraseInLine=function(e){switch(this._restrictCursor(),e.params[0]){case 0:this._eraseInBufferLine(this._bufferService.buffer.y,this._bufferService.buffer.x,this._bufferService.cols);break;case 1:this._eraseInBufferLine(this._bufferService.buffer.y,0,this._bufferService.buffer.x+1);break;case 2:this._eraseInBufferLine(this._bufferService.buffer.y,0,this._bufferService.cols)}this._dirtyRowService.markDirty(this._bufferService.buffer.y)},t.prototype.insertLines=function(e){this._restrictCursor();var t=e.params[0]||1,i=this._bufferService.buffer;if(!(i.y>i.scrollBottom||i.yi.scrollBottom||i.yt.scrollBottom||t.yt.scrollBottom||t.yt.scrollBottom||t.yt.scrollBottom||t.y0||(this._is("xterm")||this._is("rxvt-unicode")||this._is("screen")?this._coreService.triggerDataEvent(o.C0.ESC+"[?1;2c"):this._is("linux")&&this._coreService.triggerDataEvent(o.C0.ESC+"[?6c"))},t.prototype.sendDeviceAttributesSecondary=function(e){e.params[0]>0||(this._is("xterm")?this._coreService.triggerDataEvent(o.C0.ESC+"[>0;276;0c"):this._is("rxvt-unicode")?this._coreService.triggerDataEvent(o.C0.ESC+"[>85;95;0c"):this._is("linux")?this._coreService.triggerDataEvent(e.params[0]+"c"):this._is("screen")&&this._coreService.triggerDataEvent(o.C0.ESC+"[>83;40003;0c"))},t.prototype._is=function(e){return 0===(this._optionsService.options.termName+"").indexOf(e)},t.prototype.setMode=function(e){for(var t=0;t=2||2===n[1]&&s+r>=5)break;n[1]&&(r=1)}while(++s+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,0===e&&(t.fg&=-268435457),t.updateExtended()},t.prototype.charAttributes=function(e){if(1===e.length&&0===e.params[0])return this._curAttrData.fg=d.DEFAULT_ATTR_DATA.fg,void(this._curAttrData.bg=d.DEFAULT_ATTR_DATA.bg);for(var t,i=e.length,n=this._curAttrData,r=0;r=30&&t<=37?(n.fg&=-50331904,n.fg|=16777216|t-30):t>=40&&t<=47?(n.bg&=-50331904,n.bg|=16777216|t-40):t>=90&&t<=97?(n.fg&=-50331904,n.fg|=16777224|t-90):t>=100&&t<=107?(n.bg&=-50331904,n.bg|=16777224|t-100):0===t?(n.fg=d.DEFAULT_ATTR_DATA.fg,n.bg=d.DEFAULT_ATTR_DATA.bg):1===t?n.fg|=134217728:3===t?n.bg|=67108864:4===t?(n.fg|=268435456,this._processUnderline(e.hasSubParams(r)?e.getSubParams(r)[0]:1,n)):5===t?n.fg|=536870912:7===t?n.fg|=67108864:8===t?n.fg|=1073741824:2===t?n.bg|=134217728:21===t?this._processUnderline(2,n):22===t?(n.fg&=-134217729,n.bg&=-134217729):23===t?n.bg&=-67108865:24===t?n.fg&=-268435457:25===t?n.fg&=-536870913:27===t?n.fg&=-67108865:28===t?n.fg&=-1073741825:39===t?(n.fg&=-67108864,n.fg|=16777215&d.DEFAULT_ATTR_DATA.fg):49===t?(n.bg&=-67108864,n.bg|=16777215&d.DEFAULT_ATTR_DATA.bg):38===t||48===t||58===t?r+=this._extractColor(e,r,n):59===t?(n.extended=n.extended.clone(),n.extended.underlineColor=-1,n.updateExtended()):100===t?(n.fg&=-67108864,n.fg|=16777215&d.DEFAULT_ATTR_DATA.fg,n.bg&=-67108864,n.bg|=16777215&d.DEFAULT_ATTR_DATA.bg):this._logService.debug("Unknown SGR attribute: %d.",t)},t.prototype.deviceStatus=function(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(o.C0.ESC+"[0n");break;case 6:this._coreService.triggerDataEvent(o.C0.ESC+"["+(this._bufferService.buffer.y+1)+";"+(this._bufferService.buffer.x+1)+"R")}},t.prototype.deviceStatusPrivate=function(e){switch(e.params[0]){case 6:this._coreService.triggerDataEvent(o.C0.ESC+"[?"+(this._bufferService.buffer.y+1)+";"+(this._bufferService.buffer.x+1)+"R")}},t.prototype.softReset=function(e){this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._bufferService.buffer.scrollTop=0,this._bufferService.buffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=d.DEFAULT_ATTR_DATA.clone(),this._coreService.reset(),this._charsetService.reset(),this._bufferService.buffer.savedX=0,this._bufferService.buffer.savedY=this._bufferService.buffer.ybase,this._bufferService.buffer.savedCurAttrData.fg=this._curAttrData.fg,this._bufferService.buffer.savedCurAttrData.bg=this._curAttrData.bg,this._bufferService.buffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1},t.prototype.setCursorStyle=function(e){var t=e.params[0]||1;switch(t){case 1:case 2:this._optionsService.options.cursorStyle="block";break;case 3:case 4:this._optionsService.options.cursorStyle="underline";break;case 5:case 6:this._optionsService.options.cursorStyle="bar"}this._optionsService.options.cursorBlink=t%2==1},t.prototype.setScrollRegion=function(e){var t,i=e.params[0]||1;(e.length<2||(t=e.params[1])>this._bufferService.rows||0===t)&&(t=this._bufferService.rows),t>i&&(this._bufferService.buffer.scrollTop=i-1,this._bufferService.buffer.scrollBottom=t-1,this._setCursor(0,0))},t.prototype.windowOptions=function(e){if(b(e.params[0],this._optionsService.options.windowOptions)){var t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:2!==t&&this._onRequestWindowsOptionsReport.fire(s.GET_WIN_SIZE_PIXELS);break;case 16:this._onRequestWindowsOptionsReport.fire(s.GET_CELL_SIZE_PIXELS);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(o.C0.ESC+"[8;"+this._bufferService.rows+";"+this._bufferService.cols+"t");break;case 22:0!==t&&2!==t||(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>10&&this._windowTitleStack.shift()),0!==t&&1!==t||(this._iconNameStack.push(this._iconName),this._iconNameStack.length>10&&this._iconNameStack.shift());break;case 23:0!==t&&2!==t||this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),0!==t&&1!==t||this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop())}}},t.prototype.saveCursor=function(e){this._bufferService.buffer.savedX=this._bufferService.buffer.x,this._bufferService.buffer.savedY=this._bufferService.buffer.ybase+this._bufferService.buffer.y,this._bufferService.buffer.savedCurAttrData.fg=this._curAttrData.fg,this._bufferService.buffer.savedCurAttrData.bg=this._curAttrData.bg,this._bufferService.buffer.savedCharset=this._charsetService.charset},t.prototype.restoreCursor=function(e){this._bufferService.buffer.x=this._bufferService.buffer.savedX||0,this._bufferService.buffer.y=Math.max(this._bufferService.buffer.savedY-this._bufferService.buffer.ybase,0),this._curAttrData.fg=this._bufferService.buffer.savedCurAttrData.fg,this._curAttrData.bg=this._bufferService.buffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._bufferService.buffer.savedCharset&&(this._charsetService.charset=this._bufferService.buffer.savedCharset),this._restrictCursor()},t.prototype.setTitle=function(e){this._windowTitle=e,this._onTitleChange.fire(e)},t.prototype.setIconName=function(e){this._iconName=e},t.prototype.nextLine=function(){this._bufferService.buffer.x=0,this.index()},t.prototype.keypadApplicationMode=function(){this._logService.debug("Serial port requested application keypad."),this._coreService.decPrivateModes.applicationKeypad=!0,this._onRequestSyncScrollBar.fire()},t.prototype.keypadNumericMode=function(){this._logService.debug("Switching back to normal keypad."),this._coreService.decPrivateModes.applicationKeypad=!1,this._onRequestSyncScrollBar.fire()},t.prototype.selectDefaultCharset=function(){this._charsetService.setgLevel(0),this._charsetService.setgCharset(0,a.DEFAULT_CHARSET)},t.prototype.selectCharset=function(e){2===e.length?"/"!==e[0]&&this._charsetService.setgCharset(y[e[0]],a.CHARSETS[e[1]]||a.DEFAULT_CHARSET):this.selectDefaultCharset()},t.prototype.index=function(){this._restrictCursor();var e=this._bufferService.buffer;this._bufferService.buffer.y++,e.y===e.scrollBottom+1?(e.y--,this._onRequestScroll.fire(this._eraseAttrData())):e.y>=this._bufferService.rows&&(e.y=this._bufferService.rows-1),this._restrictCursor()},t.prototype.tabSet=function(){this._bufferService.buffer.tabs[this._bufferService.buffer.x]=!0},t.prototype.reverseIndex=function(){this._restrictCursor();var e=this._bufferService.buffer;e.y===e.scrollTop?(e.lines.shiftElements(e.ybase+e.y,e.scrollBottom-e.scrollTop,1),e.lines.set(e.ybase+e.y,e.getBlankLine(this._eraseAttrData())),this._dirtyRowService.markRangeDirty(e.scrollTop,e.scrollBottom)):(e.y--,this._restrictCursor())},t.prototype.fullReset=function(){this._parser.reset(),this._onRequestReset.fire()},t.prototype.reset=function(){this._curAttrData=d.DEFAULT_ATTR_DATA.clone(),this._eraseAttrDataInternal=d.DEFAULT_ATTR_DATA.clone()},t.prototype._eraseAttrData=function(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=67108863&this._curAttrData.bg,this._eraseAttrDataInternal},t.prototype.setgLevel=function(e){this._charsetService.setgLevel(e)},t.prototype.screenAlignmentPattern=function(){var e=new _.CellData;e.content=1<<22|"E".charCodeAt(0),e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg;var t=this._bufferService.buffer;this._setCursor(0,0);for(var i=0;i256)throw new Error("maxSubParamsLength must not be greater than 256");this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}return e.fromArray=function(t){var i=new e;if(!t.length)return i;for(var n=t[0]instanceof Array?1:0;n>8,n=255&this._subParamsIdx[t];n-i>0&&e.push(Array.prototype.slice.call(this._subParams,i,n))}return e},e.prototype.reset=function(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1},e.prototype.addParam=function(e){if(this._digitIsSub=!1,this.length>=this.maxLength)this._rejectDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>2147483647?2147483647:e}},e.prototype.addSubParam=function(e){if(this._digitIsSub=!0,this.length)if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength)this._rejectSubDigits=!0;else{if(e<-1)throw new Error("values lesser than -1 are not allowed");this._subParams[this._subParamsLength++]=e>2147483647?2147483647:e,this._subParamsIdx[this.length-1]++}},e.prototype.hasSubParams=function(e){return(255&this._subParamsIdx[e])-(this._subParamsIdx[e]>>8)>0},e.prototype.getSubParams=function(e){var t=this._subParamsIdx[e]>>8,i=255&this._subParamsIdx[e];return i-t>0?this._subParams.subarray(t,i):null},e.prototype.getSubParamsAll=function(){for(var e={},t=0;t>8,n=255&this._subParamsIdx[t];n-i>0&&(e[t]=this._subParams.slice(i,n))}return e},e.prototype.addDigit=function(e){var t;if(!(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)){var i=this._digitIsSub?this._subParams:this.params,n=i[t-1];i[t-1]=~n?Math.min(10*n+e,2147483647):e}},e}();t.Params=n},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.OscHandler=t.OscParser=void 0;var n=i(23),r=i(8),s=function(){function e(){this._state=0,this._id=-1,this._handlers=Object.create(null),this._handlerFb=function(){}}return e.prototype.addHandler=function(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);var i=this._handlers[e];return i.push(t),{dispose:function(){var e=i.indexOf(t);-1!==e&&i.splice(e,1)}}},e.prototype.setHandler=function(e,t){this._handlers[e]=[t]},e.prototype.clearHandler=function(e){this._handlers[e]&&delete this._handlers[e]},e.prototype.setHandlerFallback=function(e){this._handlerFb=e},e.prototype.dispose=function(){this._handlers=Object.create(null),this._handlerFb=function(){}},e.prototype.reset=function(){2===this._state&&this.end(!1),this._id=-1,this._state=0},e.prototype._start=function(){var e=this._handlers[this._id];if(e)for(var t=e.length-1;t>=0;t--)e[t].start();else this._handlerFb(this._id,"START")},e.prototype._put=function(e,t,i){var n=this._handlers[this._id];if(n)for(var s=n.length-1;s>=0;s--)n[s].put(e,t,i);else this._handlerFb(this._id,"PUT",r.utf32ToString(e,t,i))},e.prototype._end=function(e){var t=this._handlers[this._id];if(t){for(var i=t.length-1;i>=0&&!1===t[i].end(e);i--);for(i--;i>=0;i--)t[i].end(!1)}else this._handlerFb(this._id,"END",e)},e.prototype.start=function(){this.reset(),this._id=-1,this._state=1},e.prototype.put=function(e,t,i){if(3!==this._state){if(1===this._state)for(;t0&&this._put(e,t,i)}},e.prototype.end=function(e){0!==this._state&&(3!==this._state&&(1===this._state&&this._start(),this._end(e)),this._id=-1,this._state=0)},e}();t.OscParser=s;var o=function(){function e(e){this._handler=e,this._data="",this._hitLimit=!1}return e.prototype.start=function(){this._data="",this._hitLimit=!1},e.prototype.put=function(e,t,i){this._hitLimit||(this._data+=r.utf32ToString(e,t,i),this._data.length>n.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))},e.prototype.end=function(e){var t;return this._hitLimit?t=!1:e&&(t=this._handler(this._data)),this._data="",this._hitLimit=!1,t},e}();t.OscHandler=o},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PAYLOAD_LIMIT=void 0,t.PAYLOAD_LIMIT=1e7},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DcsHandler=t.DcsParser=void 0;var n=i(8),r=i(21),s=i(23),o=[],a=function(){function e(){this._handlers=Object.create(null),this._active=o,this._ident=0,this._handlerFb=function(){}}return e.prototype.dispose=function(){this._handlers=Object.create(null),this._handlerFb=function(){}},e.prototype.addHandler=function(e,t){void 0===this._handlers[e]&&(this._handlers[e]=[]);var i=this._handlers[e];return i.push(t),{dispose:function(){var e=i.indexOf(t);-1!==e&&i.splice(e,1)}}},e.prototype.setHandler=function(e,t){this._handlers[e]=[t]},e.prototype.clearHandler=function(e){this._handlers[e]&&delete this._handlers[e]},e.prototype.setHandlerFallback=function(e){this._handlerFb=e},e.prototype.reset=function(){this._active.length&&this.unhook(!1),this._active=o,this._ident=0},e.prototype.hook=function(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||o,this._active.length)for(var i=this._active.length-1;i>=0;i--)this._active[i].hook(t);else this._handlerFb(this._ident,"HOOK",t)},e.prototype.put=function(e,t,i){if(this._active.length)for(var r=this._active.length-1;r>=0;r--)this._active[r].put(e,t,i);else this._handlerFb(this._ident,"PUT",n.utf32ToString(e,t,i))},e.prototype.unhook=function(e){if(this._active.length){for(var t=this._active.length-1;t>=0&&!1===this._active[t].unhook(e);t--);for(t--;t>=0;t--)this._active[t].unhook(!1)}else this._handlerFb(this._ident,"UNHOOK",e);this._active=o,this._ident=0},e}();t.DcsParser=a;var l=function(){function e(e){this._handler=e,this._data="",this._hitLimit=!1}return e.prototype.hook=function(e){this._params=e.clone(),this._data="",this._hitLimit=!1},e.prototype.put=function(e,t,i){this._hitLimit||(this._data+=n.utf32ToString(e,t,i),this._data.length>s.PAYLOAD_LIMIT&&(this._data="",this._hitLimit=!0))},e.prototype.unhook=function(e){var t;return this._hitLimit?t=!1:e&&(t=this._handler(this._data,this._params||new r.Params)),this._params=void 0,this._data="",this._hitLimit=!1,t},e}();t.DcsHandler=l},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.removeTerminalFromCache=t.acquireCharAtlas=void 0;var n=i(26),r=i(43),s=[];t.acquireCharAtlas=function(e,t,i,o,a){for(var l=n.generateConfig(o,a,e,i),c=0;c=0){if(n.configEquals(u.config,l))return u.atlas;1===u.ownedBy.length?(u.atlas.dispose(),s.splice(c,1)):u.ownedBy.splice(h,1);break}}for(c=0;c1)for(var u=this._getJoinedRanges(n,a,s,t,r),d=0;d1)for(u=this._getJoinedRanges(n,a,s,t,r),d=0;d=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new n.CellData)},e.prototype.translateToString=function(e,t,i){return this._line.translateToString(e,t,i)},e}(),d=function(){function e(e){this._core=e}return e.prototype.registerCsiHandler=function(e,t){return this._core.addCsiHandler(e,(function(e){return t(e.toArray())}))},e.prototype.addCsiHandler=function(e,t){return this.registerCsiHandler(e,t)},e.prototype.registerDcsHandler=function(e,t){return this._core.addDcsHandler(e,(function(e,i){return t(e,i.toArray())}))},e.prototype.addDcsHandler=function(e,t){return this.registerDcsHandler(e,t)},e.prototype.registerEscHandler=function(e,t){return this._core.addEscHandler(e,t)},e.prototype.addEscHandler=function(e,t){return this.registerEscHandler(e,t)},e.prototype.registerOscHandler=function(e,t){return this._core.addOscHandler(e,t)},e.prototype.addOscHandler=function(e,t){return this.registerOscHandler(e,t)},e}(),f=function(){function e(e){this._core=e}return e.prototype.register=function(e){this._core.unicodeService.register(e)},Object.defineProperty(e.prototype,"versions",{get:function(){return this._core.unicodeService.versions},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"activeVersion",{get:function(){return this._core.unicodeService.activeVersion},set:function(e){this._core.unicodeService.activeVersion=e},enumerable:!1,configurable:!0}),e}()},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.Terminal=void 0;var s=i(36),o=i(37),a=i(38),l=i(12),c=i(19),h=i(40),u=i(50),d=i(51),f=i(11),p=i(7),_=i(18),m=i(54),g=i(55),v=i(56),y=i(57),b=i(59),C=i(0),w=i(16),S=i(27),k=i(60),x=i(5),E=i(61),A=i(62),T=i(63),O=i(64),R=i(65),L="undefined"!=typeof window?window.document:null,D=function(e){function t(t){void 0===t&&(t={});var i=e.call(this,t)||this;return i.browser=f,i._keyDownHandled=!1,i._onCursorMove=new C.EventEmitter,i._onKey=new C.EventEmitter,i._onRender=new C.EventEmitter,i._onSelectionChange=new C.EventEmitter,i._onTitleChange=new C.EventEmitter,i._onFocus=new C.EventEmitter,i._onBlur=new C.EventEmitter,i._onA11yCharEmitter=new C.EventEmitter,i._onA11yTabEmitter=new C.EventEmitter,i._setup(),i.linkifier=i._instantiationService.createInstance(u.Linkifier),i.linkifier2=i.register(i._instantiationService.createInstance(T.Linkifier2)),i.register(i._inputHandler.onRequestBell((function(){return i.bell()}))),i.register(i._inputHandler.onRequestRefreshRows((function(e,t){return i.refresh(e,t)}))),i.register(i._inputHandler.onRequestReset((function(){return i.reset()}))),i.register(i._inputHandler.onRequestScroll((function(e,t){return i.scroll(e,t||void 0)}))),i.register(i._inputHandler.onRequestWindowsOptionsReport((function(e){return i._reportWindowsOptions(e)}))),i.register(C.forwardEvent(i._inputHandler.onCursorMove,i._onCursorMove)),i.register(C.forwardEvent(i._inputHandler.onTitleChange,i._onTitleChange)),i.register(C.forwardEvent(i._inputHandler.onA11yChar,i._onA11yCharEmitter)),i.register(C.forwardEvent(i._inputHandler.onA11yTab,i._onA11yTabEmitter)),i.register(i._bufferService.onResize((function(e){return i._afterResize(e.cols,e.rows)}))),i}return r(t,e),Object.defineProperty(t.prototype,"options",{get:function(){return this.optionsService.options},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onCursorMove",{get:function(){return this._onCursorMove.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onKey",{get:function(){return this._onKey.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRender",{get:function(){return this._onRender.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onSelectionChange",{get:function(){return this._onSelectionChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onTitleChange",{get:function(){return this._onTitleChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onFocus",{get:function(){return this._onFocus.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onBlur",{get:function(){return this._onBlur.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yChar",{get:function(){return this._onA11yCharEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onA11yTab",{get:function(){return this._onA11yTabEmitter.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){var t,i,n;this._isDisposed||(e.prototype.dispose.call(this),null===(t=this._renderService)||void 0===t||t.dispose(),this._customKeyEventHandler=void 0,this.write=function(){},null===(n=null===(i=this.element)||void 0===i?void 0:i.parentNode)||void 0===n||n.removeChild(this.element))},t.prototype._setup=function(){e.prototype._setup.call(this),this._customKeyEventHandler=void 0},Object.defineProperty(t.prototype,"buffer",{get:function(){return this.buffers.active},enumerable:!1,configurable:!0}),t.prototype.focus=function(){this.textarea&&this.textarea.focus({preventScroll:!0})},t.prototype._updateOptions=function(t){var i,n,r,s;switch(e.prototype._updateOptions.call(this,t),t){case"fontFamily":case"fontSize":null===(i=this._renderService)||void 0===i||i.clear(),null===(n=this._charSizeService)||void 0===n||n.measure();break;case"cursorBlink":case"cursorStyle":this.refresh(this.buffer.y,this.buffer.y);break;case"drawBoldTextInBrightColors":case"letterSpacing":case"lineHeight":case"fontWeight":case"fontWeightBold":case"minimumContrastRatio":this._renderService&&(this._renderService.clear(),this._renderService.onResize(this.cols,this.rows),this.refresh(0,this.rows-1));break;case"rendererType":this._renderService&&(this._renderService.setRenderer(this._createRenderer()),this._renderService.onResize(this.cols,this.rows));break;case"scrollback":null===(r=this.viewport)||void 0===r||r.syncScrollArea();break;case"screenReaderMode":this.optionsService.options.screenReaderMode?!this._accessibilityManager&&this._renderService&&(this._accessibilityManager=new v.AccessibilityManager(this,this._renderService)):(null===(s=this._accessibilityManager)||void 0===s||s.dispose(),this._accessibilityManager=void 0);break;case"tabStopWidth":this.buffers.setupTabStops();break;case"theme":this._setTheme(this.optionsService.options.theme)}},t.prototype._onTextAreaFocus=function(e){this._coreService.decPrivateModes.sendFocus&&this._coreService.triggerDataEvent(l.C0.ESC+"[I"),this.updateCursorStyle(e),this.element.classList.add("focus"),this._showCursor(),this._onFocus.fire()},t.prototype.blur=function(){var e;return null===(e=this.textarea)||void 0===e?void 0:e.blur()},t.prototype._onTextAreaBlur=function(){this.textarea.value="",this.refresh(this.buffer.y,this.buffer.y),this._coreService.decPrivateModes.sendFocus&&this._coreService.triggerDataEvent(l.C0.ESC+"[O"),this.element.classList.remove("focus"),this._onBlur.fire()},t.prototype._syncTextArea=function(){if(this.textarea&&this.buffer.isCursorInViewport&&!this._compositionHelper.isComposing){var e=Math.ceil(this._charSizeService.height*this.optionsService.options.lineHeight),t=this._bufferService.buffer.y*e;this.textarea.style.left=this._bufferService.buffer.x*this._charSizeService.width+"px",this.textarea.style.top=t+"px",this.textarea.style.width=this._charSizeService.width+"px",this.textarea.style.height=e+"px",this.textarea.style.lineHeight=e+"px",this.textarea.style.zIndex="-5"}},t.prototype._initGlobal=function(){var e=this;this._bindKeys(),this.register(p.addDisposableDomListener(this.element,"copy",(function(t){e.hasSelection()&&a.copyHandler(t,e._selectionService)})));var t=function(t){return a.handlePasteEvent(t,e.textarea,e._coreService)};this.register(p.addDisposableDomListener(this.textarea,"paste",t)),this.register(p.addDisposableDomListener(this.element,"paste",t)),this.register(f.isFirefox?p.addDisposableDomListener(this.element,"mousedown",(function(t){2===t.button&&a.rightClickHandler(t,e.textarea,e.screenElement,e._selectionService,e.options.rightClickSelectsWord)})):p.addDisposableDomListener(this.element,"contextmenu",(function(t){a.rightClickHandler(t,e.textarea,e.screenElement,e._selectionService,e.options.rightClickSelectsWord)}))),f.isLinux&&this.register(p.addDisposableDomListener(this.element,"auxclick",(function(t){1===t.button&&a.moveTextAreaUnderMouseCursor(t,e.textarea,e.screenElement)})))},t.prototype._bindKeys=function(){var e=this;this.register(p.addDisposableDomListener(this.textarea,"keyup",(function(t){return e._keyUp(t)}),!0)),this.register(p.addDisposableDomListener(this.textarea,"keydown",(function(t){return e._keyDown(t)}),!0)),this.register(p.addDisposableDomListener(this.textarea,"keypress",(function(t){return e._keyPress(t)}),!0)),this.register(p.addDisposableDomListener(this.textarea,"compositionstart",(function(){return e._compositionHelper.compositionstart()}))),this.register(p.addDisposableDomListener(this.textarea,"compositionupdate",(function(t){return e._compositionHelper.compositionupdate(t)}))),this.register(p.addDisposableDomListener(this.textarea,"compositionend",(function(){return e._compositionHelper.compositionend()}))),this.register(this.onRender((function(){return e._compositionHelper.updateCompositionElements()}))),this.register(this.onRender((function(t){return e._queueLinkification(t.start,t.end)})))},t.prototype.open=function(e){var t=this;if(!e)throw new Error("Terminal requires a parent element.");L.body.contains(e)||this._logService.debug("Terminal.open was called on an element that was not attached to the DOM"),this._document=e.ownerDocument,this.element=this._document.createElement("div"),this.element.dir="ltr",this.element.classList.add("terminal"),this.element.classList.add("xterm"),this.element.setAttribute("tabindex","0"),e.appendChild(this.element);var i=L.createDocumentFragment();this._viewportElement=L.createElement("div"),this._viewportElement.classList.add("xterm-viewport"),i.appendChild(this._viewportElement),this._viewportScrollArea=L.createElement("div"),this._viewportScrollArea.classList.add("xterm-scroll-area"),this._viewportElement.appendChild(this._viewportScrollArea),this.screenElement=L.createElement("div"),this.screenElement.classList.add("xterm-screen"),this._helperContainer=L.createElement("div"),this._helperContainer.classList.add("xterm-helpers"),this.screenElement.appendChild(this._helperContainer),i.appendChild(this.screenElement),this.textarea=L.createElement("textarea"),this.textarea.classList.add("xterm-helper-textarea"),this.textarea.setAttribute("aria-label",_.promptLabel),this.textarea.setAttribute("aria-multiline","false"),this.textarea.setAttribute("autocorrect","off"),this.textarea.setAttribute("autocapitalize","off"),this.textarea.setAttribute("spellcheck","false"),this.textarea.tabIndex=0,this.register(p.addDisposableDomListener(this.textarea,"focus",(function(e){return t._onTextAreaFocus(e)}))),this.register(p.addDisposableDomListener(this.textarea,"blur",(function(){return t._onTextAreaBlur()}))),this._helperContainer.appendChild(this.textarea);var n=this._instantiationService.createInstance(O.CoreBrowserService,this.textarea);this._instantiationService.setService(x.ICoreBrowserService,n),this._charSizeService=this._instantiationService.createInstance(E.CharSizeService,this._document,this._helperContainer),this._instantiationService.setService(x.ICharSizeService,this._charSizeService),this._compositionView=L.createElement("div"),this._compositionView.classList.add("composition-view"),this._compositionHelper=this._instantiationService.createInstance(s.CompositionHelper,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this.element.appendChild(i),this._theme=this.options.theme||this._theme,this._colorManager=new S.ColorManager(L,this.options.allowTransparency),this.register(this.optionsService.onOptionChange((function(e){return t._colorManager.onOptionsChange(e)}))),this._colorManager.setTheme(this._theme);var r=this._createRenderer();this._renderService=this.register(this._instantiationService.createInstance(k.RenderService,r,this.rows,this.screenElement)),this._instantiationService.setService(x.IRenderService,this._renderService),this.register(this._renderService.onRenderedBufferChange((function(e){return t._onRender.fire(e)}))),this.onResize((function(e){return t._renderService.resize(e.cols,e.rows)})),this._soundService=this._instantiationService.createInstance(m.SoundService),this._instantiationService.setService(x.ISoundService,this._soundService),this._mouseService=this._instantiationService.createInstance(A.MouseService),this._instantiationService.setService(x.IMouseService,this._mouseService),this.viewport=this._instantiationService.createInstance(o.Viewport,(function(e,i){return t.scrollLines(e,i)}),this._viewportElement,this._viewportScrollArea),this.viewport.onThemeChange(this._colorManager.colors),this.register(this._inputHandler.onRequestSyncScrollBar((function(){return t.viewport.syncScrollArea()}))),this.register(this.viewport),this.register(this.onCursorMove((function(){t._renderService.onCursorMove(),t._syncTextArea()}))),this.register(this.onResize((function(){return t._renderService.onResize(t.cols,t.rows)}))),this.register(this.onBlur((function(){return t._renderService.onBlur()}))),this.register(this.onFocus((function(){return t._renderService.onFocus()}))),this.register(this._renderService.onDimensionsChange((function(){return t.viewport.syncScrollArea()}))),this._selectionService=this.register(this._instantiationService.createInstance(d.SelectionService,this.element,this.screenElement)),this._instantiationService.setService(x.ISelectionService,this._selectionService),this.register(this._selectionService.onRequestScrollLines((function(e){return t.scrollLines(e.amount,e.suppressScrollEvent)}))),this.register(this._selectionService.onSelectionChange((function(){return t._onSelectionChange.fire()}))),this.register(this._selectionService.onRequestRedraw((function(e){return t._renderService.onSelectionChanged(e.start,e.end,e.columnSelectMode)}))),this.register(this._selectionService.onLinuxMouseSelection((function(e){t.textarea.value=e,t.textarea.focus(),t.textarea.select()}))),this.register(this.onScroll((function(){t.viewport.syncScrollArea(),t._selectionService.refresh()}))),this.register(p.addDisposableDomListener(this._viewportElement,"scroll",(function(){return t._selectionService.refresh()}))),this._mouseZoneManager=this._instantiationService.createInstance(g.MouseZoneManager,this.element,this.screenElement),this.register(this._mouseZoneManager),this.register(this.onScroll((function(){return t._mouseZoneManager.clearAll()}))),this.linkifier.attachToDom(this.element,this._mouseZoneManager),this.linkifier2.attachToDom(this.element,this._mouseService,this._renderService),this.register(p.addDisposableDomListener(this.element,"mousedown",(function(e){return t._selectionService.onMouseDown(e)}))),this._coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add("enable-mouse-events")):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager=new v.AccessibilityManager(this,this._renderService)),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()},t.prototype._createRenderer=function(){switch(this.options.rendererType){case"canvas":return this._instantiationService.createInstance(h.Renderer,this._colorManager.colors,this.screenElement,this.linkifier,this.linkifier2);case"dom":return this._instantiationService.createInstance(y.DomRenderer,this._colorManager.colors,this.element,this.screenElement,this._viewportElement,this.linkifier,this.linkifier2);default:throw new Error('Unrecognized rendererType "'+this.options.rendererType+'"')}},t.prototype._setTheme=function(e){var t,i,n;this._theme=e,null===(t=this._colorManager)||void 0===t||t.setTheme(e),null===(i=this._renderService)||void 0===i||i.setColors(this._colorManager.colors),null===(n=this.viewport)||void 0===n||n.onThemeChange(this._colorManager.colors)},t.prototype.bindMouse=function(){var e=this,t=this,i=this.element;function n(e){var i,n,r=t._mouseService.getRawByteCoords(e,t.screenElement,t.cols,t.rows);if(!r)return!1;switch(e.overrideType||e.type){case"mousemove":n=32,void 0===e.buttons?(i=3,void 0!==e.button&&(i=e.button<3?e.button:3)):i=1&e.buttons?0:4&e.buttons?1:2&e.buttons?2:3;break;case"mouseup":n=0,i=e.button<3?e.button:3;break;case"mousedown":n=1,i=e.button<3?e.button:3;break;case"wheel":0!==e.deltaY&&(n=e.deltaY<0?0:1),i=4;break;default:return!1}return!(void 0===n||void 0===i||i>4)&&t._coreMouseService.triggerMouseEvent({col:r.x-33,row:r.y-33,button:i,action:n,ctrl:e.ctrlKey,alt:e.altKey,shift:e.shiftKey})}var r={mouseup:null,wheel:null,mousedrag:null,mousemove:null},s=function(t){return n(t),t.buttons||(e._document.removeEventListener("mouseup",r.mouseup),r.mousedrag&&e._document.removeEventListener("mousemove",r.mousedrag)),e.cancel(t)},o=function(t){return n(t),t.preventDefault(),e.cancel(t)},a=function(e){e.buttons&&n(e)},c=function(e){e.buttons||n(e)};this.register(this._coreMouseService.onProtocolChange((function(t){t?("debug"===e.optionsService.options.logLevel&&e._logService.debug("Binding to mouse events:",e._coreMouseService.explainEvents(t)),e.element.classList.add("enable-mouse-events"),e._selectionService.disable()):(e._logService.debug("Unbinding from mouse events."),e.element.classList.remove("enable-mouse-events"),e._selectionService.enable()),8&t?r.mousemove||(i.addEventListener("mousemove",c),r.mousemove=c):(i.removeEventListener("mousemove",r.mousemove),r.mousemove=null),16&t?r.wheel||(i.addEventListener("wheel",o,{passive:!1}),r.wheel=o):(i.removeEventListener("wheel",r.wheel),r.wheel=null),2&t?r.mouseup||(r.mouseup=s):(e._document.removeEventListener("mouseup",r.mouseup),r.mouseup=null),4&t?r.mousedrag||(r.mousedrag=a):(e._document.removeEventListener("mousemove",r.mousedrag),r.mousedrag=null)}))),this._coreMouseService.activeProtocol=this._coreMouseService.activeProtocol,this.register(p.addDisposableDomListener(i,"mousedown",(function(t){if(t.preventDefault(),e.focus(),e._coreMouseService.areMouseEventsActive&&!e._selectionService.shouldForceSelection(t))return n(t),r.mouseup&&e._document.addEventListener("mouseup",r.mouseup),r.mousedrag&&e._document.addEventListener("mousemove",r.mousedrag),e.cancel(t)}))),this.register(p.addDisposableDomListener(i,"wheel",(function(t){if(r.wheel);else if(!e.buffer.hasScrollback){var i=e.viewport.getLinesScrolled(t);if(0===i)return;for(var n=l.C0.ESC+(e._coreService.decPrivateModes.applicationCursorKeys?"O":"[")+(t.deltaY<0?"A":"B"),s="",o=0;o47)},t.prototype._keyUp=function(e){this._customKeyEventHandler&&!1===this._customKeyEventHandler(e)||(function(e){return 16===e.keyCode||17===e.keyCode||18===e.keyCode}(e)||this.focus(),this.updateCursorStyle(e))},t.prototype._keyPress=function(e){var t;if(this._keyDownHandled)return!1;if(this._customKeyEventHandler&&!1===this._customKeyEventHandler(e))return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(null==e.which)t=e.keyCode;else{if(0===e.which||0===e.charCode)return!1;t=e.which}return!(!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e)||(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this._coreService.triggerDataEvent(t,!0),0))},t.prototype.bell=function(){this._soundBell()&&this._soundService.playBellSound()},t.prototype.resize=function(t,i){t!==this.cols||i!==this.rows?e.prototype.resize.call(this,t,i):this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure()},t.prototype._afterResize=function(e,t){var i,n;null===(i=this._charSizeService)||void 0===i||i.measure(),null===(n=this.viewport)||void 0===n||n.syncScrollArea(!0)},t.prototype.clear=function(){if(0!==this.buffer.ybase||0!==this.buffer.y){this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(var e=1;e=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CompositionHelper=void 0;var s=i(5),o=i(1),a=function(){function e(e,t,i,n,r,s){this._textarea=e,this._compositionView=t,this._bufferService=i,this._optionsService=n,this._charSizeService=r,this._coreService=s,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0}}return Object.defineProperty(e.prototype,"isComposing",{get:function(){return this._isComposing},enumerable:!1,configurable:!0}),e.prototype.compositionstart=function(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent="",this._compositionView.classList.add("active")},e.prototype.compositionupdate=function(e){var t=this;this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout((function(){t._compositionPosition.end=t._textarea.value.length}),0)},e.prototype.compositionend=function(){this._finalizeComposition(!0)},e.prototype.keydown=function(e){if(this._isComposing||this._isSendingComposition){if(229===e.keyCode)return!1;if(16===e.keyCode||17===e.keyCode||18===e.keyCode)return!1;this._finalizeComposition(!1)}return 229!==e.keyCode||(this._handleAnyTextareaChanges(),!1)},e.prototype._finalizeComposition=function(e){var t=this;if(this._compositionView.classList.remove("active"),this._isComposing=!1,e){var i={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout((function(){var e;t._isSendingComposition&&(t._isSendingComposition=!1,e=t._isComposing?t._textarea.value.substring(i.start,i.end):t._textarea.value.substring(i.start),t._coreService.triggerDataEvent(e,!0))}),0)}else{this._isSendingComposition=!1;var n=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(n,!0)}},e.prototype._handleAnyTextareaChanges=function(){var e=this,t=this._textarea.value;setTimeout((function(){if(!e._isComposing){var i=e._textarea.value.replace(t,"");i.length>0&&e._coreService.triggerDataEvent(i,!0)}}),0)},e.prototype.updateCompositionElements=function(e){var t=this;if(this._isComposing){if(this._bufferService.buffer.isCursorInViewport){var i=Math.ceil(this._charSizeService.height*this._optionsService.options.lineHeight),n=this._bufferService.buffer.y*i,r=this._bufferService.buffer.x*this._charSizeService.width;this._compositionView.style.left=r+"px",this._compositionView.style.top=n+"px",this._compositionView.style.height=i+"px",this._compositionView.style.lineHeight=i+"px",this._compositionView.style.fontFamily=this._optionsService.options.fontFamily,this._compositionView.style.fontSize=this._optionsService.options.fontSize+"px";var s=this._compositionView.getBoundingClientRect();this._textarea.style.left=r+"px",this._textarea.style.top=n+"px",this._textarea.style.width=s.width+"px",this._textarea.style.height=s.height+"px",this._textarea.style.lineHeight=s.height+"px"}e||setTimeout((function(){return t.updateCompositionElements(!0)}),0)}},n([r(2,o.IBufferService),r(3,o.IOptionsService),r(4,s.ICharSizeService),r(5,o.ICoreService)],e)}();t.CompositionHelper=a},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Viewport=void 0;var a=i(2),l=i(7),c=i(5),h=i(1),u=function(e){function t(t,i,n,r,s,o,a){var c=e.call(this)||this;return c._scrollLines=t,c._viewportElement=i,c._scrollArea=n,c._bufferService=r,c._optionsService=s,c._charSizeService=o,c._renderService=a,c.scrollBarWidth=0,c._currentRowHeight=0,c._lastRecordedBufferLength=0,c._lastRecordedViewportHeight=0,c._lastRecordedBufferHeight=0,c._lastTouchY=0,c._lastScrollTop=0,c._wheelPartialScroll=0,c._refreshAnimationFrame=null,c._ignoreNextScrollEvent=!1,c.scrollBarWidth=c._viewportElement.offsetWidth-c._scrollArea.offsetWidth||15,c.register(l.addDisposableDomListener(c._viewportElement,"scroll",c._onScroll.bind(c))),setTimeout((function(){return c.syncScrollArea()}),0),c}return r(t,e),t.prototype.onThemeChange=function(e){this._viewportElement.style.backgroundColor=e.background.css},t.prototype._refresh=function(e){var t=this;if(e)return this._innerRefresh(),void(null!==this._refreshAnimationFrame&&cancelAnimationFrame(this._refreshAnimationFrame));null===this._refreshAnimationFrame&&(this._refreshAnimationFrame=requestAnimationFrame((function(){return t._innerRefresh()})))},t.prototype._innerRefresh=function(){if(this._charSizeService.height>0){this._currentRowHeight=this._renderService.dimensions.scaledCellHeight/window.devicePixelRatio,this._lastRecordedViewportHeight=this._viewportElement.offsetHeight;var e=Math.round(this._currentRowHeight*this._lastRecordedBufferLength)+(this._lastRecordedViewportHeight-this._renderService.dimensions.canvasHeight);this._lastRecordedBufferHeight!==e&&(this._lastRecordedBufferHeight=e,this._scrollArea.style.height=this._lastRecordedBufferHeight+"px")}var t=this._bufferService.buffer.ydisp*this._currentRowHeight;this._viewportElement.scrollTop!==t&&(this._ignoreNextScrollEvent=!0,this._viewportElement.scrollTop=t),this._refreshAnimationFrame=null},t.prototype.syncScrollArea=function(e){if(void 0===e&&(e=!1),this._lastRecordedBufferLength!==this._bufferService.buffer.lines.length)return this._lastRecordedBufferLength=this._bufferService.buffer.lines.length,void this._refresh(e);this._lastRecordedViewportHeight===this._renderService.dimensions.canvasHeight&&this._lastScrollTop===this._bufferService.buffer.ydisp*this._currentRowHeight&&this._lastScrollTop===this._viewportElement.scrollTop&&this._renderService.dimensions.scaledCellHeight/window.devicePixelRatio===this._currentRowHeight||this._refresh(e)},t.prototype._onScroll=function(e){if(this._lastScrollTop=this._viewportElement.scrollTop,this._viewportElement.offsetParent)if(this._ignoreNextScrollEvent)this._ignoreNextScrollEvent=!1;else{var t=Math.round(this._lastScrollTop/this._currentRowHeight)-this._bufferService.buffer.ydisp;this._scrollLines(t,!0)}},t.prototype._bubbleScroll=function(e,t){return!(t<0&&0!==this._viewportElement.scrollTop||t>0&&this._viewportElement.scrollTop+this._lastRecordedViewportHeight0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(t*=this._bufferService.rows),t},t.prototype._applyScrollModifier=function(e,t){var i=this._optionsService.options.fastScrollModifier;return"alt"===i&&t.altKey||"ctrl"===i&&t.ctrlKey||"shift"===i&&t.shiftKey?e*this._optionsService.options.fastScrollSensitivity*this._optionsService.options.scrollSensitivity:e*this._optionsService.options.scrollSensitivity},t.prototype.onTouchStart=function(e){this._lastTouchY=e.touches[0].pageY},t.prototype.onTouchMove=function(e){var t=this._lastTouchY-e.touches[0].pageY;return this._lastTouchY=e.touches[0].pageY,0!==t&&(this._viewportElement.scrollTop+=t,this._bubbleScroll(e,t))},s([o(3,h.IBufferService),o(4,h.IOptionsService),o(5,c.ICharSizeService),o(6,c.IRenderService)],t)}(a.Disposable);t.Viewport=u},function(e,t,i){"use strict";function n(e){return e.replace(/\r?\n/g,"\r")}function r(e,t){return t?"\x1b[200~"+e+"\x1b[201~":e}function s(e,t,i){e=r(e=n(e),i.decPrivateModes.bracketedPasteMode),i.triggerDataEvent(e,!0),t.value=""}function o(e,t,i){var n=i.getBoundingClientRect(),r=e.clientX-n.left-10,s=e.clientY-n.top-10;t.style.width="20px",t.style.height="20px",t.style.left=r+"px",t.style.top=s+"px",t.style.zIndex="1000",t.focus()}Object.defineProperty(t,"__esModule",{value:!0}),t.rightClickHandler=t.moveTextAreaUnderMouseCursor=t.paste=t.handlePasteEvent=t.copyHandler=t.bracketTextForPaste=t.prepareTextForTerminal=void 0,t.prepareTextForTerminal=n,t.bracketTextForPaste=r,t.copyHandler=function(e,t){e.clipboardData&&e.clipboardData.setData("text/plain",t.selectionText),e.preventDefault()},t.handlePasteEvent=function(e,t,i){e.stopPropagation(),e.clipboardData&&s(e.clipboardData.getData("text/plain"),t,i)},t.paste=s,t.moveTextAreaUnderMouseCursor=o,t.rightClickHandler=function(e,t,i,n,r){o(e,t,i),r&&!n.isClickInSelection(e)&&n.selectWordAtCursor(e),t.value=n.selectionText,t.select()}},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.EscapeSequenceParser=t.VT500_TRANSITION_TABLE=t.TransitionTable=void 0;var s=i(2),o=i(15),a=i(21),l=i(22),c=i(24),h=function(){function e(e){this.table=new Uint8Array(e)}return e.prototype.setDefault=function(e,t){o.fill(this.table,e<<4|t)},e.prototype.add=function(e,t,i,n){this.table[t<<8|e]=i<<4|n},e.prototype.addMany=function(e,t,i,n){for(var r=0;r1)throw new Error("only one byte as prefix supported");if((i=e.prefix.charCodeAt(0))&&60>i||i>63)throw new Error("prefix must be in range 0x3c .. 0x3f")}if(e.intermediates){if(e.intermediates.length>2)throw new Error("only two bytes as intermediates are supported");for(var n=0;nr||r>47)throw new Error("intermediate must be in range 0x20 .. 0x2f");i<<=8,i|=r}}if(1!==e.final.length)throw new Error("final must be a single byte");var s=e.final.charCodeAt(0);if(t[0]>s||s>t[1])throw new Error("final must be in range "+t[0]+" .. "+t[1]);return(i<<=8)|s},i.prototype.identToString=function(e){for(var t=[];e;)t.push(String.fromCharCode(255&e)),e>>=8;return t.reverse().join("")},i.prototype.dispose=function(){this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null),this._oscParser.dispose(),this._dcsParser.dispose()},i.prototype.setPrintHandler=function(e){this._printHandler=e},i.prototype.clearPrintHandler=function(){this._printHandler=this._printHandlerFb},i.prototype.addEscHandler=function(e,t){var i=this._identifier(e,[48,126]);void 0===this._escHandlers[i]&&(this._escHandlers[i]=[]);var n=this._escHandlers[i];return n.push(t),{dispose:function(){var e=n.indexOf(t);-1!==e&&n.splice(e,1)}}},i.prototype.setEscHandler=function(e,t){this._escHandlers[this._identifier(e,[48,126])]=[t]},i.prototype.clearEscHandler=function(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]},i.prototype.setEscHandlerFallback=function(e){this._escHandlerFb=e},i.prototype.setExecuteHandler=function(e,t){this._executeHandlers[e.charCodeAt(0)]=t},i.prototype.clearExecuteHandler=function(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]},i.prototype.setExecuteHandlerFallback=function(e){this._executeHandlerFb=e},i.prototype.addCsiHandler=function(e,t){var i=this._identifier(e);void 0===this._csiHandlers[i]&&(this._csiHandlers[i]=[]);var n=this._csiHandlers[i];return n.push(t),{dispose:function(){var e=n.indexOf(t);-1!==e&&n.splice(e,1)}}},i.prototype.setCsiHandler=function(e,t){this._csiHandlers[this._identifier(e)]=[t]},i.prototype.clearCsiHandler=function(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]},i.prototype.setCsiHandlerFallback=function(e){this._csiHandlerFb=e},i.prototype.addDcsHandler=function(e,t){return this._dcsParser.addHandler(this._identifier(e),t)},i.prototype.setDcsHandler=function(e,t){this._dcsParser.setHandler(this._identifier(e),t)},i.prototype.clearDcsHandler=function(e){this._dcsParser.clearHandler(this._identifier(e))},i.prototype.setDcsHandlerFallback=function(e){this._dcsParser.setHandlerFallback(e)},i.prototype.addOscHandler=function(e,t){return this._oscParser.addHandler(e,t)},i.prototype.setOscHandler=function(e,t){this._oscParser.setHandler(e,t)},i.prototype.clearOscHandler=function(e){this._oscParser.clearHandler(e)},i.prototype.setOscHandlerFallback=function(e){this._oscParser.setHandlerFallback(e)},i.prototype.setErrorHandler=function(e){this._errorHandler=e},i.prototype.clearErrorHandler=function(){this._errorHandler=this._errorHandlerFb},i.prototype.reset=function(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingCodepoint=0},i.prototype.parse=function(e,t){for(var i=0,n=0,r=this.currentState,s=this._oscParser,o=this._dcsParser,a=this._collect,l=this._params,c=this._transitions.table,h=0;h>4){case 2:for(var u=h+1;;++u){if(u>=t||(i=e[u])<32||i>126&&i<160){this._printHandler(e,h,u),h=u-1;break}if(++u>=t||(i=e[u])<32||i>126&&i<160){this._printHandler(e,h,u),h=u-1;break}if(++u>=t||(i=e[u])<32||i>126&&i<160){this._printHandler(e,h,u),h=u-1;break}if(++u>=t||(i=e[u])<32||i>126&&i<160){this._printHandler(e,h,u),h=u-1;break}}break;case 3:this._executeHandlers[i]?this._executeHandlers[i]():this._executeHandlerFb(i),this.precedingCodepoint=0;break;case 0:break;case 1:if(this._errorHandler({position:h,code:i,currentState:r,collect:a,params:l,abort:!1}).abort)return;break;case 7:for(var d=this._csiHandlers[a<<8|i],f=d?d.length-1:-1;f>=0&&!1===d[f](l);f--);f<0&&this._csiHandlerFb(a<<8|i,l),this.precedingCodepoint=0;break;case 8:do{switch(i){case 59:l.addParam(0);break;case 58:l.addSubParam(-1);break;default:l.addDigit(i-48)}}while(++h47&&i<60);h--;break;case 9:a<<=8,a|=i;break;case 10:for(var p=this._escHandlers[a<<8|i],_=p?p.length-1:-1;_>=0&&!1===p[_]();_--);_<0&&this._escHandlerFb(a<<8|i),this.precedingCodepoint=0;break;case 11:l.reset(),l.addParam(0),a=0;break;case 12:o.hook(a<<8|i,l);break;case 13:for(var m=h+1;;++m)if(m>=t||24===(i=e[m])||26===i||27===i||i>127&&i<160){o.put(e,h,m),h=m-1;break}break;case 14:o.unhook(24!==i&&26!==i),27===i&&(n|=1),l.reset(),l.addParam(0),a=0,this.precedingCodepoint=0;break;case 4:s.start();break;case 5:for(var g=h+1;;g++)if(g>=t||(i=e[g])<32||i>127&&i<=159){s.put(e,h,g),h=g-1;break}break;case 6:s.end(24!==i&&26!==i),27===i&&(n|=1),l.reset(),l.addParam(0),a=0,this.precedingCodepoint=0}r=15&n}this._collect=a,this.currentState=r},i}(s.Disposable);t.EscapeSequenceParser=u},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Renderer=void 0;var a=i(41),l=i(47),c=i(48),h=i(49),u=i(29),d=i(2),f=i(5),p=i(1),_=i(25),m=i(0),g=1,v=function(e){function t(t,i,n,r,s,o,d,f,p){var _=e.call(this)||this;_._colors=t,_._screenElement=i,_._bufferService=s,_._charSizeService=o,_._optionsService=d,_._id=g++,_._onRequestRedraw=new m.EventEmitter;var v=_._optionsService.options.allowTransparency;return _._characterJoinerRegistry=new u.CharacterJoinerRegistry(_._bufferService),_._renderLayers=[new a.TextRenderLayer(_._screenElement,0,_._colors,_._characterJoinerRegistry,v,_._id,_._bufferService,d),new l.SelectionRenderLayer(_._screenElement,1,_._colors,_._id,_._bufferService,d),new h.LinkRenderLayer(_._screenElement,2,_._colors,_._id,n,r,_._bufferService,d),new c.CursorRenderLayer(_._screenElement,3,_._colors,_._id,_._onRequestRedraw,_._bufferService,d,f,p)],_.dimensions={scaledCharWidth:0,scaledCharHeight:0,scaledCellWidth:0,scaledCellHeight:0,scaledCharLeft:0,scaledCharTop:0,scaledCanvasWidth:0,scaledCanvasHeight:0,canvasWidth:0,canvasHeight:0,actualCellWidth:0,actualCellHeight:0},_._devicePixelRatio=window.devicePixelRatio,_._updateDimensions(),_.onOptionsChanged(),_}return r(t,e),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return this._onRequestRedraw.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){for(var t=0,i=this._renderLayers;t0&&h===a[0][0]){d=!0;var p=a.shift();u=new c.JoinedCellData(this._workCell,o.translateToString(!0,p[0],p[1]),p[1]-p[0]),f=p[1]-1}!d&&this._isOverlapping(u)&&fthis._characterWidth;return this._ctx.restore(),this._characterOverlapCache[t]=i,i},t}(o.BaseRenderLayer);t.TextRenderLayer=u},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.GridCache=void 0;var n=function(){function e(){this.cache=[]}return e.prototype.resize=function(e,t){for(var i=0;i>>24,r=t.rgba>>>16&255,s=t.rgba>>>8&255,o=0;o=this.capacity)this._unlinkNode(i=this._head),delete this._map[i.key],i.key=e,i.value=t,this._map[e]=i;else{var n=this._nodePool;n.length>0?((i=n.pop()).key=e,i.value=t):i={prev:null,next:null,key:e,value:t},this._map[e]=i,this.size++}this._appendNode(i)},e}();t.LRUMap=n},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionRenderLayer=void 0;var s=function(e){function t(t,i,n,r,s,o){var a=e.call(this,t,"selection",i,!0,n,r,s,o)||this;return a._clearState(),a}return r(t,e),t.prototype._clearState=function(){this._state={start:void 0,end:void 0,columnSelectMode:void 0,ydisp:void 0}},t.prototype.resize=function(t){e.prototype.resize.call(this,t),this._clearState()},t.prototype.reset=function(){this._state.start&&this._state.end&&(this._clearState(),this._clearAll())},t.prototype.onSelectionChanged=function(e,t,i){if(this._didStateChange(e,t,i,this._bufferService.buffer.ydisp))if(this._clearAll(),e&&t){var n=e[1]-this._bufferService.buffer.ydisp,r=t[1]-this._bufferService.buffer.ydisp,s=Math.max(n,0),o=Math.min(r,this._bufferService.rows-1);if(s>=this._bufferService.rows||o<0)this._state.ydisp=this._bufferService.buffer.ydisp;else{if(this._ctx.fillStyle=this._colors.selectionTransparent.css,i){var a=e[0];this._fillCells(a,s,t[0]-a,o-s+1)}else{this._fillCells(a=n===s?e[0]:0,s,(s===r?t[0]:this._bufferService.cols)-a,1);var l=Math.max(o-s-1,0);this._fillCells(0,s+1,this._bufferService.cols,l),s!==o&&this._fillCells(0,o,r===o?t[0]:this._bufferService.cols,1)}this._state.start=[e[0],e[1]],this._state.end=[t[0],t[1]],this._state.columnSelectMode=i,this._state.ydisp=this._bufferService.buffer.ydisp}}else this._clearState()},t.prototype._didStateChange=function(e,t,i,n){return!this._areCoordinatesEqual(e,this._state.start)||!this._areCoordinatesEqual(t,this._state.end)||i!==this._state.columnSelectMode||n!==this._state.ydisp},t.prototype._areCoordinatesEqual=function(e,t){return!(!e||!t)&&e[0]===t[0]&&e[1]===t[1]},t}(i(13).BaseRenderLayer);t.SelectionRenderLayer=s},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.CursorRenderLayer=void 0;var s=i(13),o=i(4),a=function(e){function t(t,i,n,r,s,a,l,c,h){var u=e.call(this,t,"cursor",i,!0,n,r,a,l)||this;return u._onRequestRedraw=s,u._coreService=c,u._coreBrowserService=h,u._cell=new o.CellData,u._state={x:0,y:0,isFocused:!1,style:"",width:0},u._cursorRenderers={bar:u._renderBarCursor.bind(u),block:u._renderBlockCursor.bind(u),underline:u._renderUnderlineCursor.bind(u)},u}return r(t,e),t.prototype.resize=function(t){e.prototype.resize.call(this,t),this._state={x:0,y:0,isFocused:!1,style:"",width:0}},t.prototype.reset=function(){this._clearCursor(),this._cursorBlinkStateManager&&(this._cursorBlinkStateManager.dispose(),this._cursorBlinkStateManager=void 0,this.onOptionsChanged())},t.prototype.onBlur=function(){this._cursorBlinkStateManager&&this._cursorBlinkStateManager.pause(),this._onRequestRedraw.fire({start:this._bufferService.buffer.y,end:this._bufferService.buffer.y})},t.prototype.onFocus=function(){this._cursorBlinkStateManager?this._cursorBlinkStateManager.resume():this._onRequestRedraw.fire({start:this._bufferService.buffer.y,end:this._bufferService.buffer.y})},t.prototype.onOptionsChanged=function(){var e,t=this;this._optionsService.options.cursorBlink?this._cursorBlinkStateManager||(this._cursorBlinkStateManager=new l(this._coreBrowserService.isFocused,(function(){t._render(!0)}))):(null===(e=this._cursorBlinkStateManager)||void 0===e||e.dispose(),this._cursorBlinkStateManager=void 0),this._onRequestRedraw.fire({start:this._bufferService.buffer.y,end:this._bufferService.buffer.y})},t.prototype.onCursorMove=function(){this._cursorBlinkStateManager&&this._cursorBlinkStateManager.restartBlinkAnimation()},t.prototype.onGridChanged=function(e,t){!this._cursorBlinkStateManager||this._cursorBlinkStateManager.isPaused?this._render(!1):this._cursorBlinkStateManager.restartBlinkAnimation()},t.prototype._render=function(e){if(this._coreService.isCursorInitialized&&!this._coreService.isCursorHidden){var t=this._bufferService.buffer.ybase+this._bufferService.buffer.y,i=t-this._bufferService.buffer.ydisp;if(i<0||i>=this._bufferService.rows)this._clearCursor();else{var n=Math.min(this._bufferService.buffer.x,this._bufferService.cols-1);if(this._bufferService.buffer.lines.get(t).loadCell(n,this._cell),void 0!==this._cell.content){if(!this._coreBrowserService.isFocused){this._clearCursor(),this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css;var r=this._optionsService.options.cursorStyle;return r&&"block"!==r?this._cursorRenderers[r](n,i,this._cell):this._renderBlurCursor(n,i,this._cell),this._ctx.restore(),this._state.x=n,this._state.y=i,this._state.isFocused=!1,this._state.style=r,void(this._state.width=this._cell.getWidth())}if(!this._cursorBlinkStateManager||this._cursorBlinkStateManager.isCursorVisible){if(this._state){if(this._state.x===n&&this._state.y===i&&this._state.isFocused===this._coreBrowserService.isFocused&&this._state.style===this._optionsService.options.cursorStyle&&this._state.width===this._cell.getWidth())return;this._clearCursor()}this._ctx.save(),this._cursorRenderers[this._optionsService.options.cursorStyle||"block"](n,i,this._cell),this._ctx.restore(),this._state.x=n,this._state.y=i,this._state.isFocused=!1,this._state.style=this._optionsService.options.cursorStyle,this._state.width=this._cell.getWidth()}else this._clearCursor()}}}else this._clearCursor()},t.prototype._clearCursor=function(){this._state&&(this._clearCells(this._state.x,this._state.y,this._state.width,1),this._state={x:0,y:0,isFocused:!1,style:"",width:0})},t.prototype._renderBarCursor=function(e,t,i){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillLeftLineAtCell(e,t,this._optionsService.options.cursorWidth),this._ctx.restore()},t.prototype._renderBlockCursor=function(e,t,i){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillCells(e,t,i.getWidth(),1),this._ctx.fillStyle=this._colors.cursorAccent.css,this._fillCharTrueColor(i,e,t),this._ctx.restore()},t.prototype._renderUnderlineCursor=function(e,t,i){this._ctx.save(),this._ctx.fillStyle=this._colors.cursor.css,this._fillBottomLineAtCells(e,t),this._ctx.restore()},t.prototype._renderBlurCursor=function(e,t,i){this._ctx.save(),this._ctx.strokeStyle=this._colors.cursor.css,this._strokeRectAtCell(e,t,i.getWidth(),1),this._ctx.restore()},t}(s.BaseRenderLayer);t.CursorRenderLayer=a;var l=function(){function e(e,t){this._renderCallback=t,this.isCursorVisible=!0,e&&this._restartInterval()}return Object.defineProperty(e.prototype,"isPaused",{get:function(){return!(this._blinkStartTimeout||this._blinkInterval)},enumerable:!1,configurable:!0}),e.prototype.dispose=function(){this._blinkInterval&&(window.clearInterval(this._blinkInterval),this._blinkInterval=void 0),this._blinkStartTimeout&&(window.clearTimeout(this._blinkStartTimeout),this._blinkStartTimeout=void 0),this._animationFrame&&(window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)},e.prototype.restartBlinkAnimation=function(){var e=this;this.isPaused||(this._animationTimeRestarted=Date.now(),this.isCursorVisible=!0,this._animationFrame||(this._animationFrame=window.requestAnimationFrame((function(){e._renderCallback(),e._animationFrame=void 0}))))},e.prototype._restartInterval=function(e){var t=this;void 0===e&&(e=600),this._blinkInterval&&window.clearInterval(this._blinkInterval),this._blinkStartTimeout=window.setTimeout((function(){if(t._animationTimeRestarted){var e=600-(Date.now()-t._animationTimeRestarted);if(t._animationTimeRestarted=void 0,e>0)return void t._restartInterval(e)}t.isCursorVisible=!1,t._animationFrame=window.requestAnimationFrame((function(){t._renderCallback(),t._animationFrame=void 0})),t._blinkInterval=window.setInterval((function(){if(t._animationTimeRestarted){var e=600-(Date.now()-t._animationTimeRestarted);return t._animationTimeRestarted=void 0,void t._restartInterval(e)}t.isCursorVisible=!t.isCursorVisible,t._animationFrame=window.requestAnimationFrame((function(){t._renderCallback(),t._animationFrame=void 0}))}),600)}),e)},e.prototype.pause=function(){this.isCursorVisible=!0,this._blinkInterval&&(window.clearInterval(this._blinkInterval),this._blinkInterval=void 0),this._blinkStartTimeout&&(window.clearTimeout(this._blinkStartTimeout),this._blinkStartTimeout=void 0),this._animationFrame&&(window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)},e.prototype.resume=function(){this.pause(),this._animationTimeRestarted=void 0,this._restartInterval(),this.restartBlinkAnimation()},e}()},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.LinkRenderLayer=void 0;var s=i(13),o=i(9),a=i(26),l=function(e){function t(t,i,n,r,s,o,a,l){var c=e.call(this,t,"link",i,!0,n,r,a,l)||this;return s.onShowLinkUnderline((function(e){return c._onShowLinkUnderline(e)})),s.onHideLinkUnderline((function(e){return c._onHideLinkUnderline(e)})),o.onShowLinkUnderline((function(e){return c._onShowLinkUnderline(e)})),o.onHideLinkUnderline((function(e){return c._onHideLinkUnderline(e)})),c}return r(t,e),t.prototype.resize=function(t){e.prototype.resize.call(this,t),this._state=void 0},t.prototype.reset=function(){this._clearCurrentLink()},t.prototype._clearCurrentLink=function(){if(this._state){this._clearCells(this._state.x1,this._state.y1,this._state.cols-this._state.x1,1);var e=this._state.y2-this._state.y1-1;e>0&&this._clearCells(0,this._state.y1+1,this._state.cols,e),this._clearCells(0,this._state.y2,this._state.x2,1),this._state=void 0}},t.prototype._onShowLinkUnderline=function(e){if(this._ctx.fillStyle=e.fg===o.INVERTED_DEFAULT_COLOR?this._colors.background.css:e.fg&&a.is256Color(e.fg)?this._colors.ansi[e.fg].css:this._colors.foreground.css,e.y1===e.y2)this._fillBottomLineAtCells(e.x1,e.y1,e.x2-e.x1);else{this._fillBottomLineAtCells(e.x1,e.y1,e.cols-e.x1);for(var t=e.y1+1;t=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseZone=t.Linkifier=void 0;var s=i(0),o=i(1),a=function(){function e(e,t,i){this._bufferService=e,this._logService=t,this._unicodeService=i,this._linkMatchers=[],this._nextLinkMatcherId=0,this._onShowLinkUnderline=new s.EventEmitter,this._onHideLinkUnderline=new s.EventEmitter,this._onLinkTooltip=new s.EventEmitter,this._rowsToLinkify={start:void 0,end:void 0}}return Object.defineProperty(e.prototype,"onShowLinkUnderline",{get:function(){return this._onShowLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onHideLinkUnderline",{get:function(){return this._onHideLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onLinkTooltip",{get:function(){return this._onLinkTooltip.event},enumerable:!1,configurable:!0}),e.prototype.attachToDom=function(e,t){this._element=e,this._mouseZoneManager=t},e.prototype.linkifyRows=function(t,i){var n=this;this._mouseZoneManager&&(void 0===this._rowsToLinkify.start||void 0===this._rowsToLinkify.end?(this._rowsToLinkify.start=t,this._rowsToLinkify.end=i):(this._rowsToLinkify.start=Math.min(this._rowsToLinkify.start,t),this._rowsToLinkify.end=Math.max(this._rowsToLinkify.end,i)),this._mouseZoneManager.clearAll(t,i),this._rowsTimeoutId&&clearTimeout(this._rowsTimeoutId),this._rowsTimeoutId=setTimeout((function(){return n._linkifyRows()}),e._timeBeforeLatency))},e.prototype._linkifyRows=function(){this._rowsTimeoutId=void 0;var e=this._bufferService.buffer;if(void 0!==this._rowsToLinkify.start&&void 0!==this._rowsToLinkify.end){var t=e.ydisp+this._rowsToLinkify.start;if(!(t>=e.lines.length)){for(var i=e.ydisp+Math.min(this._rowsToLinkify.end,this._bufferService.rows)+1,n=Math.ceil(2e3/this._bufferService.cols),r=this._bufferService.buffer.iterator(!1,t,i,n,n);r.hasNext();)for(var s=r.next(),o=0;o=0;t--)if(e.priority<=this._linkMatchers[t].priority)return void this._linkMatchers.splice(t+1,0,e);this._linkMatchers.splice(0,0,e)}else this._linkMatchers.push(e)},e.prototype.deregisterLinkMatcher=function(e){for(var t=0;t>9&511:void 0;i.validationCallback?i.validationCallback(a,(function(e){r._rowsTimeoutId||e&&r._addLink(c[1],c[0]-r._bufferService.buffer.ydisp,a,i,d)})):l._addLink(c[1],c[0]-l._bufferService.buffer.ydisp,a,i,d)},l=this;null!==(n=s.exec(t))&&"break"!==a(););},e.prototype._addLink=function(e,t,i,n,r){var s=this;if(this._mouseZoneManager&&this._element){var o=this._unicodeService.getStringCellWidth(i),a=e%this._bufferService.cols,c=t+Math.floor(e/this._bufferService.cols),h=(a+o)%this._bufferService.cols,u=c+Math.floor((a+o)/this._bufferService.cols);0===h&&(h=this._bufferService.cols,u--),this._mouseZoneManager.add(new l(a+1,c+1,h+1,u+1,(function(e){if(n.handler)return n.handler(e,i);var t=window.open();t?(t.opener=null,t.location.href=i):console.warn("Opening link blocked as opener could not be cleared")}),(function(){s._onShowLinkUnderline.fire(s._createLinkHoverEvent(a,c,h,u,r)),s._element.classList.add("xterm-cursor-pointer")}),(function(e){s._onLinkTooltip.fire(s._createLinkHoverEvent(a,c,h,u,r)),n.hoverTooltipCallback&&n.hoverTooltipCallback(e,i,{start:{x:a,y:c},end:{x:h,y:u}})}),(function(){s._onHideLinkUnderline.fire(s._createLinkHoverEvent(a,c,h,u,r)),s._element.classList.remove("xterm-cursor-pointer"),n.hoverLeaveCallback&&n.hoverLeaveCallback()}),(function(e){return!n.willLinkActivate||n.willLinkActivate(e,i)})))}},e.prototype._createLinkHoverEvent=function(e,t,i,n,r){return{x1:e,y1:t,x2:i,y2:n,cols:this._bufferService.cols,fg:r}},e._timeBeforeLatency=200,e=n([r(0,o.IBufferService),r(1,o.ILogService),r(2,o.IUnicodeService)],e)}();t.Linkifier=a;var l=function(e,t,i,n,r,s,o,a,l){this.x1=e,this.y1=t,this.x2=i,this.y2=n,this.clickCallback=r,this.hoverCallback=s,this.tooltipCallback=o,this.leaveCallback=a,this.willLinkActivate=l};t.MouseZone=l},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionService=void 0;var a=i(11),l=i(52),c=i(4),h=i(0),u=i(5),d=i(1),f=i(30),p=i(53),_=i(2),m=String.fromCharCode(160),g=new RegExp(m,"g"),v=function(e){function t(t,i,n,r,s,o,a){var u=e.call(this)||this;return u._element=t,u._screenElement=i,u._bufferService=n,u._coreService=r,u._mouseService=s,u._optionsService=o,u._renderService=a,u._dragScrollAmount=0,u._enabled=!0,u._workCell=new c.CellData,u._mouseDownTimeStamp=0,u._onLinuxMouseSelection=u.register(new h.EventEmitter),u._onRedrawRequest=u.register(new h.EventEmitter),u._onSelectionChange=u.register(new h.EventEmitter),u._onRequestScrollLines=u.register(new h.EventEmitter),u._mouseMoveListener=function(e){return u._onMouseMove(e)},u._mouseUpListener=function(e){return u._onMouseUp(e)},u._coreService.onUserInput((function(){u.hasSelection&&u.clearSelection()})),u._trimListener=u._bufferService.buffer.lines.onTrim((function(e){return u._onTrim(e)})),u.register(u._bufferService.buffers.onBufferActivate((function(e){return u._onBufferActivate(e)}))),u.enable(),u._model=new l.SelectionModel(u._bufferService),u._activeSelectionMode=0,u}return r(t,e),Object.defineProperty(t.prototype,"onLinuxMouseSelection",{get:function(){return this._onLinuxMouseSelection.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return this._onRedrawRequest.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onSelectionChange",{get:function(){return this._onSelectionChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRequestScrollLines",{get:function(){return this._onRequestScrollLines.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this._removeMouseDownListeners()},t.prototype.reset=function(){this.clearSelection()},t.prototype.disable=function(){this.clearSelection(),this._enabled=!1},t.prototype.enable=function(){this._enabled=!0},Object.defineProperty(t.prototype,"selectionStart",{get:function(){return this._model.finalSelectionStart},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"selectionEnd",{get:function(){return this._model.finalSelectionEnd},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"hasSelection",{get:function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!(!e||!t||e[0]===t[0]&&e[1]===t[1])},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"selectionText",{get:function(){var e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return"";var i=this._bufferService.buffer,n=[];if(3===this._activeSelectionMode){if(e[0]===t[0])return"";for(var r=e[1];r<=t[1];r++){var s=i.translateBufferLineToString(r,!0,e[0],t[0]);n.push(s)}}else{for(n.push(i.translateBufferLineToString(e[1],!0,e[0],e[1]===t[1]?t[0]:void 0)),r=e[1]+1;r<=t[1]-1;r++){var o=i.lines.get(r);s=i.translateBufferLineToString(r,!0),o&&o.isWrapped?n[n.length-1]+=s:n.push(s)}e[1]!==t[1]&&(o=i.lines.get(t[1]),s=i.translateBufferLineToString(t[1],!0,0,t[0]),o&&o.isWrapped?n[n.length-1]+=s:n.push(s))}return n.map((function(e){return e.replace(g," ")})).join(a.isWindows?"\r\n":"\n")},enumerable:!1,configurable:!0}),t.prototype.clearSelection=function(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()},t.prototype.refresh=function(e){var t=this;this._refreshAnimationFrame||(this._refreshAnimationFrame=window.requestAnimationFrame((function(){return t._refresh()}))),a.isLinux&&e&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)},t.prototype._refresh=function(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:3===this._activeSelectionMode})},t.prototype.isClickInSelection=function(e){var t=this._getMouseBufferCoords(e),i=this._model.finalSelectionStart,n=this._model.finalSelectionEnd;return!!(i&&n&&t)&&this._areCoordsInSelection(t,i,n)},t.prototype._areCoordsInSelection=function(e,t,i){return e[1]>t[1]&&e[1]=t[0]&&e[0]=t[0]},t.prototype.selectWordAtCursor=function(e){var t=this._getMouseBufferCoords(e);t&&(this._selectWordAt(t,!1),this._model.selectionEnd=void 0,this.refresh(!0))},t.prototype.selectAll=function(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()},t.prototype.selectLines=function(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()},t.prototype._onTrim=function(e){this._model.onTrim(e)&&this.refresh()},t.prototype._getMouseBufferCoords=function(e){var t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t},t.prototype._getMouseEventScrollAmount=function(e){var t=f.getCoordsRelativeToElement(e,this._screenElement)[1],i=this._renderService.dimensions.canvasHeight;return t>=0&&t<=i?0:(t>i&&(t-=i),t=Math.min(Math.max(t,-50),50),(t/=50)/Math.abs(t)+Math.round(14*t))},t.prototype.shouldForceSelection=function(e){return a.isMac?e.altKey&&this._optionsService.options.macOptionClickForcesSelection:e.shiftKey},t.prototype.onMouseDown=function(e){if(this._mouseDownTimeStamp=e.timeStamp,(2!==e.button||!this.hasSelection)&&0===e.button){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._onIncrementalClick(e):1===e.detail?this._onSingleClick(e):2===e.detail?this._onDoubleClick(e):3===e.detail&&this._onTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}},t.prototype._addMouseDownListeners=function(){var e=this;this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=window.setInterval((function(){return e._dragScroll()}),50)},t.prototype._removeMouseDownListeners=function(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0},t.prototype._onIncrementalClick=function(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))},t.prototype._onSingleClick=function(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),this._model.selectionStart){this._model.selectionEnd=void 0;var t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&0===t.hasWidth(this._model.selectionStart[0])&&this._model.selectionStart[0]++}},t.prototype._onDoubleClick=function(e){var t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=1,this._selectWordAt(t,!0))},t.prototype._onTripleClick=function(e){var t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))},t.prototype.shouldColumnSelect=function(e){return e.altKey&&!(a.isMac&&this._optionsService.options.macOptionClickForcesSelection)},t.prototype._onMouseMove=function(e){if(e.stopImmediatePropagation(),this._model.selectionStart){var t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),this._model.selectionEnd){2===this._activeSelectionMode?this._model.selectionEnd[0]=this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));var i=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(3!==this._activeSelectionMode&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}},t.prototype._onMouseUp=function(e){var t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&t<500&&e.altKey){if(this._bufferService.buffer.ybase===this._bufferService.buffer.ydisp){var i=this._mouseService.getCoords(e,this._element,this._bufferService.cols,this._bufferService.rows,!1);if(i&&void 0!==i[0]&&void 0!==i[1]){var n=p.moveToCellSequence(i[0]-1,i[1]-1,this._bufferService,this._coreService.decPrivateModes.applicationCursorKeys);this._coreService.triggerDataEvent(n,!0)}}}else this.hasSelection&&this._onSelectionChange.fire()},t.prototype._onBufferActivate=function(e){var t=this;this.clearSelection(),this._trimListener.dispose(),this._trimListener=e.activeBuffer.lines.onTrim((function(e){return t._onTrim(e)}))},t.prototype._convertViewportColToCharacterIndex=function(e,t){for(var i=t[0],n=0;t[0]>=n;n++){var r=e.loadCell(n,this._workCell).getChars().length;0===this._workCell.getWidth()?i--:r>1&&t[0]!==n&&(i+=r-1)}return i},t.prototype.setSelection=function(e,t,i){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=i,this.refresh()},t.prototype._getWordAt=function(e,t,i,n){if(void 0===i&&(i=!0),void 0===n&&(n=!0),!(e[0]>=this._bufferService.cols)){var r=this._bufferService.buffer,s=r.lines.get(e[1]);if(s){var o=r.translateBufferLineToString(e[1],!1),a=this._convertViewportColToCharacterIndex(s,e),l=a,c=e[0]-a,h=0,u=0,d=0,f=0;if(" "===o.charAt(a)){for(;a>0&&" "===o.charAt(a-1);)a--;for(;l1&&(f+=m-1,l+=m-1);p>0&&a>0&&!this._isCharWordSeparator(s.loadCell(p-1,this._workCell));){s.loadCell(p-1,this._workCell);var g=this._workCell.getChars().length;0===this._workCell.getWidth()?(h++,p--):g>1&&(d+=g-1,a-=g-1),a--,p--}for(;_1&&(f+=v-1,l+=v-1),l++,_++}}l++;var y=a+c-h+d,b=Math.min(this._bufferService.cols,l-a+h+u-d-f);if(t||""!==o.slice(a,l).trim()){if(i&&0===y&&32!==s.getCodePoint(0)){var C=r.lines.get(e[1]-1);if(C&&s.isWrapped&&32!==C.getCodePoint(this._bufferService.cols-1)){var w=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(w){var S=this._bufferService.cols-w.start;y-=S,b+=S}}}if(n&&y+b===this._bufferService.cols&&32!==s.getCodePoint(this._bufferService.cols-1)){var k=r.lines.get(e[1]+1);if(k&&k.isWrapped&&32!==k.getCodePoint(0)){var x=this._getWordAt([0,e[1]+1],!1,!1,!0);x&&(b+=x.length)}}return{start:y,length:b}}}}},t.prototype._selectWordAt=function(e,t){var i=this._getWordAt(e,t);if(i){for(;i.start<0;)i.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[i.start,e[1]],this._model.selectionStartLength=i.length}},t.prototype._selectToWordAt=function(e){var t=this._getWordAt(e,!0);if(t){for(var i=e[1];t.start<0;)t.start+=this._bufferService.cols,i--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,i++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,i]}},t.prototype._isCharWordSeparator=function(e){return 0!==e.getWidth()&&this._optionsService.options.wordSeparator.indexOf(e.getChars())>=0},t.prototype._selectLineAt=function(e){var t=this._bufferService.buffer.getWrappedRangeForLine(e);this._model.selectionStart=[0,t.first],this._model.selectionEnd=[this._bufferService.cols,t.last],this._model.selectionStartLength=0},s([o(2,d.IBufferService),o(3,d.ICoreService),o(4,u.IMouseService),o(5,d.IOptionsService),o(6,u.IRenderService)],t)}(_.Disposable);t.SelectionService=v},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SelectionModel=void 0;var n=function(){function e(e){this._bufferService=e,this.isSelectAllActive=!1,this.selectionStartLength=0}return e.prototype.clearSelection=function(){this.selectionStart=void 0,this.selectionEnd=void 0,this.isSelectAllActive=!1,this.selectionStartLength=0},Object.defineProperty(e.prototype,"finalSelectionStart",{get:function(){return this.isSelectAllActive?[0,0]:this.selectionEnd&&this.selectionStart&&this.areSelectionValuesReversed()?this.selectionEnd:this.selectionStart},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"finalSelectionEnd",{get:function(){if(this.isSelectAllActive)return[this._bufferService.cols,this._bufferService.buffer.ybase+this._bufferService.rows-1];if(this.selectionStart){if(!this.selectionEnd||this.areSelectionValuesReversed()){var e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}return this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]?[Math.max(this.selectionStart[0]+this.selectionStartLength,this.selectionEnd[0]),this.selectionEnd[1]]:this.selectionEnd}},enumerable:!1,configurable:!0}),e.prototype.areSelectionValuesReversed=function(){var e=this.selectionStart,t=this.selectionEnd;return!(!e||!t)&&(e[1]>t[1]||e[1]===t[1]&&e[0]>t[0])},e.prototype.onTrim=function(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)},e}();t.SelectionModel=n},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.moveToCellSequence=void 0;var n=i(12);function r(e,t,i,n){var r=e-s(i,e),a=t-s(i,t);return c(Math.abs(r-a)-function(e,t,i){for(var n=0,r=e-s(i,e),a=t-s(i,t),l=0;l=0&&tt?"A":"B"}function a(e,t,i,n,r,s){for(var o=e,a=t,l="";o!==i||a!==n;)o+=r?1:-1,r&&o>s.cols-1?(l+=s.buffer.translateBufferLineToString(a,!1,e,o),o=0,e=0,a++):!r&&o<0&&(l+=s.buffer.translateBufferLineToString(a,!1,0,e+1),e=o=s.cols-1,a--);return l+s.buffer.translateBufferLineToString(a,!1,e,o)}function l(e,t){return n.C0.ESC+(t?"O":"[")+e}function c(e,t){e=Math.floor(e);for(var i="",n=0;n0?n-s(o,n):t;var d=n,f=function(e,t,i,n,o,a){var l;return l=r(i,n,o,a).length>0?n-s(o,n):t,e=i&&le?"D":"C",c(Math.abs(h-e),l(o,n));o=u>t?"D":"C";var d=Math.abs(u-t);return c(function(e,t){return t.cols-e}(u>t?e:h,i)+(d-1)*i.cols+1+((u>t?h:e)-1),l(o,n))}},function(e,t,i){"use strict";var n=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.SoundService=void 0;var s=i(1),o=function(){function e(e){this._optionsService=e}return Object.defineProperty(e,"audioContext",{get:function(){if(!e._audioContext){var t=window.AudioContext||window.webkitAudioContext;if(!t)return console.warn("Web Audio API is not supported by this browser. Consider upgrading to the latest version"),null;e._audioContext=new t}return e._audioContext},enumerable:!1,configurable:!0}),e.prototype.playBellSound=function(){var t=e.audioContext;if(t){var i=t.createBufferSource();t.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.options.bellSound)),(function(e){i.buffer=e,i.connect(t.destination),i.start(0)}))}},e.prototype._base64ToArrayBuffer=function(e){for(var t=window.atob(e),i=t.length,n=new Uint8Array(i),r=0;r=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseZoneManager=void 0;var a=i(2),l=i(7),c=i(5),h=i(1),u=function(e){function t(t,i,n,r,s,o){var a=e.call(this)||this;return a._element=t,a._screenElement=i,a._bufferService=n,a._mouseService=r,a._selectionService=s,a._optionsService=o,a._zones=[],a._areZonesActive=!1,a._lastHoverCoords=[void 0,void 0],a._initialSelectionLength=0,a.register(l.addDisposableDomListener(a._element,"mousedown",(function(e){return a._onMouseDown(e)}))),a._mouseMoveListener=function(e){return a._onMouseMove(e)},a._mouseLeaveListener=function(e){return a._onMouseLeave(e)},a._clickListener=function(e){return a._onClick(e)},a}return r(t,e),t.prototype.dispose=function(){e.prototype.dispose.call(this),this._deactivate()},t.prototype.add=function(e){this._zones.push(e),1===this._zones.length&&this._activate()},t.prototype.clearAll=function(e,t){if(0!==this._zones.length){e&&t||(e=0,t=this._bufferService.rows-1);for(var i=0;ie&&n.y1<=t+1||n.y2>e&&n.y2<=t+1||n.y1t+1)&&(this._currentZone&&this._currentZone===n&&(this._currentZone.leaveCallback(),this._currentZone=void 0),this._zones.splice(i--,1))}0===this._zones.length&&this._deactivate()}},t.prototype._activate=function(){this._areZonesActive||(this._areZonesActive=!0,this._element.addEventListener("mousemove",this._mouseMoveListener),this._element.addEventListener("mouseleave",this._mouseLeaveListener),this._element.addEventListener("click",this._clickListener))},t.prototype._deactivate=function(){this._areZonesActive&&(this._areZonesActive=!1,this._element.removeEventListener("mousemove",this._mouseMoveListener),this._element.removeEventListener("mouseleave",this._mouseLeaveListener),this._element.removeEventListener("click",this._clickListener))},t.prototype._onMouseMove=function(e){this._lastHoverCoords[0]===e.pageX&&this._lastHoverCoords[1]===e.pageY||(this._onHover(e),this._lastHoverCoords=[e.pageX,e.pageY])},t.prototype._onHover=function(e){var t=this,i=this._findZoneEventAt(e);i!==this._currentZone&&(this._currentZone&&(this._currentZone.leaveCallback(),this._currentZone=void 0,this._tooltipTimeout&&clearTimeout(this._tooltipTimeout)),i&&(this._currentZone=i,i.hoverCallback&&i.hoverCallback(e),this._tooltipTimeout=window.setTimeout((function(){return t._onTooltip(e)}),this._optionsService.options.linkTooltipHoverDuration)))},t.prototype._onTooltip=function(e){this._tooltipTimeout=void 0;var t=this._findZoneEventAt(e);t&&t.tooltipCallback&&t.tooltipCallback(e)},t.prototype._onMouseDown=function(e){if(this._initialSelectionLength=this._getSelectionLength(),this._areZonesActive){var t=this._findZoneEventAt(e);(null==t?void 0:t.willLinkActivate(e))&&(e.preventDefault(),e.stopImmediatePropagation())}},t.prototype._onMouseLeave=function(e){this._currentZone&&(this._currentZone.leaveCallback(),this._currentZone=void 0,this._tooltipTimeout&&clearTimeout(this._tooltipTimeout))},t.prototype._onClick=function(e){var t=this._findZoneEventAt(e),i=this._getSelectionLength();t&&i===this._initialSelectionLength&&(t.clickCallback(e),e.preventDefault(),e.stopImmediatePropagation())},t.prototype._getSelectionLength=function(){var e=this._selectionService.selectionText;return e?e.length:0},t.prototype._findZoneEventAt=function(e){var t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows);if(t)for(var i=t[0],n=t[1],r=0;r=s.x1&&i=s.x1||n===s.y2&&is.y1&≠)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()},t.prototype._createAccessibilityTreeNode=function(){var e=document.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e},t.prototype._onTab=function(e){for(var t=0;t0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,"\n"===e&&(this._liveRegionLineCount++,21===this._liveRegionLineCount&&(this._liveRegion.textContent+=s.tooMuchOutput)),o.isMac&&this._liveRegion.textContent&&this._liveRegion.textContent.length>0&&!this._liveRegion.parentNode&&setTimeout((function(){t._accessibilityTreeRoot.appendChild(t._liveRegion)}),0))},t.prototype._clearLiveRegion=function(){this._liveRegion.textContent="",this._liveRegionLineCount=0,o.isMac&&u.removeElementFromParent(this._liveRegion)},t.prototype._onKey=function(e){this._clearLiveRegion(),this._charsToConsume.push(e)},t.prototype._refreshRows=function(e,t){this._renderRowsDebouncer.refresh(e,t,this._terminal.rows)},t.prototype._renderRows=function(e,t){for(var i=this._terminal.buffer,n=i.lines.length.toString(),r=e;r<=t;r++){var s=i.translateBufferLineToString(i.ydisp+r,!0),o=(i.ydisp+r+1).toString(),a=this._rowElements[r];a&&(0===s.length?a.innerHTML=" ":a.textContent=s,a.setAttribute("aria-posinset",o),a.setAttribute("aria-setsize",n))}this._announceCharacters()},t.prototype._refreshRowsDimensions=function(){if(this._renderService.dimensions.actualCellHeight){this._rowElements.length!==this._terminal.rows&&this._onResize(this._terminal.rows);for(var e=0;e=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DomRenderer=void 0;var a=i(58),l=i(9),c=i(2),h=i(5),u=i(1),d=i(0),f=i(10),p=i(17),_=1,m=function(e){function t(t,i,n,r,s,o,l,c,h){var u=e.call(this)||this;return u._colors=t,u._element=i,u._screenElement=n,u._viewportElement=r,u._linkifier=s,u._linkifier2=o,u._charSizeService=l,u._optionsService=c,u._bufferService=h,u._terminalClass=_++,u._rowElements=[],u._rowContainer=document.createElement("div"),u._rowContainer.classList.add("xterm-rows"),u._rowContainer.style.lineHeight="normal",u._rowContainer.setAttribute("aria-hidden","true"),u._refreshRowElements(u._bufferService.cols,u._bufferService.rows),u._selectionContainer=document.createElement("div"),u._selectionContainer.classList.add("xterm-selection"),u._selectionContainer.setAttribute("aria-hidden","true"),u.dimensions={scaledCharWidth:0,scaledCharHeight:0,scaledCellWidth:0,scaledCellHeight:0,scaledCharLeft:0,scaledCharTop:0,scaledCanvasWidth:0,scaledCanvasHeight:0,canvasWidth:0,canvasHeight:0,actualCellWidth:0,actualCellHeight:0},u._updateDimensions(),u._injectCss(),u._rowFactory=new a.DomRendererRowFactory(document,u._optionsService,u._colors),u._element.classList.add("xterm-dom-renderer-owner-"+u._terminalClass),u._screenElement.appendChild(u._rowContainer),u._screenElement.appendChild(u._selectionContainer),u._linkifier.onShowLinkUnderline((function(e){return u._onLinkHover(e)})),u._linkifier.onHideLinkUnderline((function(e){return u._onLinkLeave(e)})),u._linkifier2.onShowLinkUnderline((function(e){return u._onLinkHover(e)})),u._linkifier2.onHideLinkUnderline((function(e){return u._onLinkLeave(e)})),u}return r(t,e),Object.defineProperty(t.prototype,"onRequestRedraw",{get:function(){return(new d.EventEmitter).event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this._element.classList.remove("xterm-dom-renderer-owner-"+this._terminalClass),p.removeElementFromParent(this._rowContainer,this._selectionContainer,this._themeStyleElement,this._dimensionsStyleElement),e.prototype.dispose.call(this)},t.prototype._updateDimensions=function(){this.dimensions.scaledCharWidth=this._charSizeService.width*window.devicePixelRatio,this.dimensions.scaledCharHeight=Math.ceil(this._charSizeService.height*window.devicePixelRatio),this.dimensions.scaledCellWidth=this.dimensions.scaledCharWidth+Math.round(this._optionsService.options.letterSpacing),this.dimensions.scaledCellHeight=Math.floor(this.dimensions.scaledCharHeight*this._optionsService.options.lineHeight),this.dimensions.scaledCharLeft=0,this.dimensions.scaledCharTop=0,this.dimensions.scaledCanvasWidth=this.dimensions.scaledCellWidth*this._bufferService.cols,this.dimensions.scaledCanvasHeight=this.dimensions.scaledCellHeight*this._bufferService.rows,this.dimensions.canvasWidth=Math.round(this.dimensions.scaledCanvasWidth/window.devicePixelRatio),this.dimensions.canvasHeight=Math.round(this.dimensions.scaledCanvasHeight/window.devicePixelRatio),this.dimensions.actualCellWidth=this.dimensions.canvasWidth/this._bufferService.cols,this.dimensions.actualCellHeight=this.dimensions.canvasHeight/this._bufferService.rows;for(var e=0,t=this._rowElements;et;)this._rowContainer.removeChild(this._rowElements.pop())},t.prototype.onResize=function(e,t){this._refreshRowElements(e,t),this._updateDimensions()},t.prototype.onCharSizeChanged=function(){this._updateDimensions()},t.prototype.onBlur=function(){this._rowContainer.classList.remove("xterm-focus")},t.prototype.onFocus=function(){this._rowContainer.classList.add("xterm-focus")},t.prototype.onSelectionChanged=function(e,t,i){for(;this._selectionContainer.children.length;)this._selectionContainer.removeChild(this._selectionContainer.children[0]);if(e&&t){var n=e[1]-this._bufferService.buffer.ydisp,r=t[1]-this._bufferService.buffer.ydisp,s=Math.max(n,0),o=Math.min(r,this._bufferService.rows-1);if(!(s>=this._bufferService.rows||o<0)){var a=document.createDocumentFragment();i?a.appendChild(this._createSelectionElement(s,e[0],t[0],o-s+1)):(a.appendChild(this._createSelectionElement(s,n===s?e[0]:0,s===r?t[0]:this._bufferService.cols)),a.appendChild(this._createSelectionElement(s+1,0,this._bufferService.cols,o-s-1)),s!==o&&a.appendChild(this._createSelectionElement(o,0,r===o?t[0]:this._bufferService.cols))),this._selectionContainer.appendChild(a)}}},t.prototype._createSelectionElement=function(e,t,i,n){void 0===n&&(n=1);var r=document.createElement("div");return r.style.height=n*this.dimensions.actualCellHeight+"px",r.style.top=e*this.dimensions.actualCellHeight+"px",r.style.left=t*this.dimensions.actualCellWidth+"px",r.style.width=this.dimensions.actualCellWidth*(i-t)+"px",r},t.prototype.onCursorMove=function(){},t.prototype.onOptionsChanged=function(){this._updateDimensions(),this._injectCss()},t.prototype.clear=function(){for(var e=0,t=this._rowElements;e=r&&(e=0,i++)}},s([o(6,h.ICharSizeService),o(7,u.IOptionsService),o(8,u.IBufferService)],t)}(c.Disposable);t.DomRenderer=m},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DomRendererRowFactory=t.CURSOR_STYLE_UNDERLINE_CLASS=t.CURSOR_STYLE_BAR_CLASS=t.CURSOR_STYLE_BLOCK_CLASS=t.CURSOR_BLINK_CLASS=t.CURSOR_CLASS=t.UNDERLINE_CLASS=t.ITALIC_CLASS=t.DIM_CLASS=t.BOLD_CLASS=void 0;var n=i(9),r=i(3),s=i(4),o=i(10);t.BOLD_CLASS="xterm-bold",t.DIM_CLASS="xterm-dim",t.ITALIC_CLASS="xterm-italic",t.UNDERLINE_CLASS="xterm-underline",t.CURSOR_CLASS="xterm-cursor",t.CURSOR_BLINK_CLASS="xterm-cursor-blink",t.CURSOR_STYLE_BLOCK_CLASS="xterm-cursor-block",t.CURSOR_STYLE_BAR_CLASS="xterm-cursor-bar",t.CURSOR_STYLE_UNDERLINE_CLASS="xterm-cursor-underline";var a=function(){function e(e,t,i){this._document=e,this._optionsService=t,this._colors=i,this._workCell=new s.CellData}return e.prototype.setColors=function(e){this._colors=e},e.prototype.createRow=function(e,i,s,a,c,h,u){for(var d=this._document.createDocumentFragment(),f=0,p=Math.min(e.length,u)-1;p>=0;p--)if(e.loadCell(p,this._workCell).getCode()!==r.NULL_CELL_CODE||i&&p===a){f=p+1;break}for(p=0;p1&&(m.style.width=h*_+"px"),i&&p===a)switch(m.classList.add(t.CURSOR_CLASS),c&&m.classList.add(t.CURSOR_BLINK_CLASS),s){case"bar":m.classList.add(t.CURSOR_STYLE_BAR_CLASS);break;case"underline":m.classList.add(t.CURSOR_STYLE_UNDERLINE_CLASS);break;default:m.classList.add(t.CURSOR_STYLE_BLOCK_CLASS)}this._workCell.isBold()&&m.classList.add(t.BOLD_CLASS),this._workCell.isItalic()&&m.classList.add(t.ITALIC_CLASS),this._workCell.isDim()&&m.classList.add(t.DIM_CLASS),this._workCell.isUnderline()&&m.classList.add(t.UNDERLINE_CLASS),m.textContent=this._workCell.isInvisible()?r.WHITESPACE_CELL_CHAR:this._workCell.getChars()||r.WHITESPACE_CELL_CHAR;var g=this._workCell.getFgColor(),v=this._workCell.getFgColorMode(),y=this._workCell.getBgColor(),b=this._workCell.getBgColorMode(),C=!!this._workCell.isInverse();if(C){var w=g;g=y,y=w;var S=v;v=b,b=S}switch(v){case 16777216:case 33554432:this._workCell.isBold()&&g<8&&this._optionsService.options.drawBoldTextInBrightColors&&(g+=8),this._applyMinimumContrast(m,this._colors.background,this._colors.ansi[g])||m.classList.add("xterm-fg-"+g);break;case 50331648:var k=o.rgba.toColor(g>>16&255,g>>8&255,255&g);this._applyMinimumContrast(m,this._colors.background,k)||this._addStyle(m,"color:#"+l(g.toString(16),"0",6));break;case 0:default:this._applyMinimumContrast(m,this._colors.background,this._colors.foreground)||C&&m.classList.add("xterm-fg-"+n.INVERTED_DEFAULT_COLOR)}switch(b){case 16777216:case 33554432:m.classList.add("xterm-bg-"+y);break;case 50331648:this._addStyle(m,"background-color:#"+l(y.toString(16),"0",6));break;case 0:default:C&&m.classList.add("xterm-bg-"+n.INVERTED_DEFAULT_COLOR)}d.appendChild(m)}}return d},e.prototype._applyMinimumContrast=function(e,t,i){if(1===this._optionsService.options.minimumContrastRatio)return!1;var n=this._colors.contrastCache.getColor(this._workCell.bg,this._workCell.fg);return void 0===n&&(n=o.color.ensureContrastRatio(t,i,this._optionsService.options.minimumContrastRatio),this._colors.contrastCache.setColor(this._workCell.bg,this._workCell.fg,null!=n?n:null)),!!n&&(this._addStyle(e,"color:"+n.css),!0)},e.prototype._addStyle=function(e,t){e.setAttribute("style",""+(e.getAttribute("style")||"")+t+";")},e}();function l(e,t,i){for(;e.length"],191:["/","?"],192:["`","~"],219:["[","{"],220:["\\","|"],221:["]","}"],222:["'",'"']};t.evaluateKeyboardEvent=function(e,t,i,s){var o={type:0,cancel:!1,key:void 0},a=(e.shiftKey?1:0)|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:"UIKeyInputUpArrow"===e.key?o.key=t?n.C0.ESC+"OA":n.C0.ESC+"[A":"UIKeyInputLeftArrow"===e.key?o.key=t?n.C0.ESC+"OD":n.C0.ESC+"[D":"UIKeyInputRightArrow"===e.key?o.key=t?n.C0.ESC+"OC":n.C0.ESC+"[C":"UIKeyInputDownArrow"===e.key&&(o.key=t?n.C0.ESC+"OB":n.C0.ESC+"[B");break;case 8:if(e.shiftKey){o.key=n.C0.BS;break}if(e.altKey){o.key=n.C0.ESC+n.C0.DEL;break}o.key=n.C0.DEL;break;case 9:if(e.shiftKey){o.key=n.C0.ESC+"[Z";break}o.key=n.C0.HT,o.cancel=!0;break;case 13:o.key=e.altKey?n.C0.ESC+n.C0.CR:n.C0.CR,o.cancel=!0;break;case 27:o.key=n.C0.ESC,e.altKey&&(o.key=n.C0.ESC+n.C0.ESC),o.cancel=!0;break;case 37:if(e.metaKey)break;a?(o.key=n.C0.ESC+"[1;"+(a+1)+"D",o.key===n.C0.ESC+"[1;3D"&&(o.key=n.C0.ESC+(i?"b":"[1;5D"))):o.key=t?n.C0.ESC+"OD":n.C0.ESC+"[D";break;case 39:if(e.metaKey)break;a?(o.key=n.C0.ESC+"[1;"+(a+1)+"C",o.key===n.C0.ESC+"[1;3C"&&(o.key=n.C0.ESC+(i?"f":"[1;5C"))):o.key=t?n.C0.ESC+"OC":n.C0.ESC+"[C";break;case 38:if(e.metaKey)break;a?(o.key=n.C0.ESC+"[1;"+(a+1)+"A",i||o.key!==n.C0.ESC+"[1;3A"||(o.key=n.C0.ESC+"[1;5A")):o.key=t?n.C0.ESC+"OA":n.C0.ESC+"[A";break;case 40:if(e.metaKey)break;a?(o.key=n.C0.ESC+"[1;"+(a+1)+"B",i||o.key!==n.C0.ESC+"[1;3B"||(o.key=n.C0.ESC+"[1;5B")):o.key=t?n.C0.ESC+"OB":n.C0.ESC+"[B";break;case 45:e.shiftKey||e.ctrlKey||(o.key=n.C0.ESC+"[2~");break;case 46:o.key=a?n.C0.ESC+"[3;"+(a+1)+"~":n.C0.ESC+"[3~";break;case 36:o.key=a?n.C0.ESC+"[1;"+(a+1)+"H":t?n.C0.ESC+"OH":n.C0.ESC+"[H";break;case 35:o.key=a?n.C0.ESC+"[1;"+(a+1)+"F":t?n.C0.ESC+"OF":n.C0.ESC+"[F";break;case 33:e.shiftKey?o.type=2:o.key=n.C0.ESC+"[5~";break;case 34:e.shiftKey?o.type=3:o.key=n.C0.ESC+"[6~";break;case 112:o.key=a?n.C0.ESC+"[1;"+(a+1)+"P":n.C0.ESC+"OP";break;case 113:o.key=a?n.C0.ESC+"[1;"+(a+1)+"Q":n.C0.ESC+"OQ";break;case 114:o.key=a?n.C0.ESC+"[1;"+(a+1)+"R":n.C0.ESC+"OR";break;case 115:o.key=a?n.C0.ESC+"[1;"+(a+1)+"S":n.C0.ESC+"OS";break;case 116:o.key=a?n.C0.ESC+"[15;"+(a+1)+"~":n.C0.ESC+"[15~";break;case 117:o.key=a?n.C0.ESC+"[17;"+(a+1)+"~":n.C0.ESC+"[17~";break;case 118:o.key=a?n.C0.ESC+"[18;"+(a+1)+"~":n.C0.ESC+"[18~";break;case 119:o.key=a?n.C0.ESC+"[19;"+(a+1)+"~":n.C0.ESC+"[19~";break;case 120:o.key=a?n.C0.ESC+"[20;"+(a+1)+"~":n.C0.ESC+"[20~";break;case 121:o.key=a?n.C0.ESC+"[21;"+(a+1)+"~":n.C0.ESC+"[21~";break;case 122:o.key=a?n.C0.ESC+"[23;"+(a+1)+"~":n.C0.ESC+"[23~";break;case 123:o.key=a?n.C0.ESC+"[24;"+(a+1)+"~":n.C0.ESC+"[24~";break;default:if(!e.ctrlKey||e.shiftKey||e.altKey||e.metaKey)if(i&&!s||!e.altKey||e.metaKey)i&&!e.altKey&&!e.ctrlKey&&e.metaKey?65===e.keyCode&&(o.type=1):e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&1===e.key.length?o.key=e.key:e.key&&e.ctrlKey&&"_"===e.key&&(o.key=n.C0.US);else{var l=r[e.keyCode],c=l&&l[e.shiftKey?1:0];c?o.key=n.C0.ESC+c:e.keyCode>=65&&e.keyCode<=90&&(o.key=n.C0.ESC+String.fromCharCode(e.ctrlKey?e.keyCode-64:e.keyCode+32))}else e.keyCode>=65&&e.keyCode<=90?o.key=String.fromCharCode(e.keyCode-64):32===e.keyCode?o.key=n.C0.NUL:e.keyCode>=51&&e.keyCode<=55?o.key=String.fromCharCode(e.keyCode-51+27):56===e.keyCode?o.key=n.C0.DEL:219===e.keyCode?o.key=n.C0.ESC:220===e.keyCode?o.key=n.C0.FS:221===e.keyCode&&(o.key=n.C0.GS)}return o}},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.RenderService=void 0;var a=i(31),l=i(0),c=i(2),h=i(32),u=i(7),d=i(1),f=i(5),p=function(e){function t(t,i,n,r,s,o){var c=e.call(this)||this;if(c._renderer=t,c._rowCount=i,c._isPaused=!1,c._needsFullRefresh=!1,c._isNextRenderRedrawOnly=!0,c._needsSelectionRefresh=!1,c._canvasWidth=0,c._canvasHeight=0,c._selectionState={start:void 0,end:void 0,columnSelectMode:!1},c._onDimensionsChange=new l.EventEmitter,c._onRender=new l.EventEmitter,c._onRefreshRequest=new l.EventEmitter,c.register({dispose:function(){return c._renderer.dispose()}}),c._renderDebouncer=new a.RenderDebouncer((function(e,t){return c._renderRows(e,t)})),c.register(c._renderDebouncer),c._screenDprMonitor=new h.ScreenDprMonitor,c._screenDprMonitor.setListener((function(){return c.onDevicePixelRatioChange()})),c.register(c._screenDprMonitor),c.register(o.onResize((function(e){return c._fullRefresh()}))),c.register(r.onOptionChange((function(){return c._renderer.onOptionsChanged()}))),c.register(s.onCharSizeChange((function(){return c.onCharSizeChanged()}))),c._renderer.onRequestRedraw((function(e){return c.refreshRows(e.start,e.end,!0)})),c.register(u.addDisposableDomListener(window,"resize",(function(){return c.onDevicePixelRatioChange()}))),"IntersectionObserver"in window){var d=new IntersectionObserver((function(e){return c._onIntersectionChange(e[e.length-1])}),{threshold:0});d.observe(n),c.register({dispose:function(){return d.disconnect()}})}return c}return r(t,e),Object.defineProperty(t.prototype,"onDimensionsChange",{get:function(){return this._onDimensionsChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRenderedBufferChange",{get:function(){return this._onRender.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onRefreshRequest",{get:function(){return this._onRefreshRequest.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"dimensions",{get:function(){return this._renderer.dimensions},enumerable:!1,configurable:!0}),t.prototype._onIntersectionChange=function(e){this._isPaused=void 0===e.isIntersecting?0===e.intersectionRatio:!e.isIntersecting,!this._isPaused&&this._needsFullRefresh&&(this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)},t.prototype.refreshRows=function(e,t,i){void 0===i&&(i=!1),this._isPaused?this._needsFullRefresh=!0:(i||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount))},t.prototype._renderRows=function(e,t){this._renderer.renderRows(e,t),this._needsSelectionRefresh&&(this._renderer.onSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),this._needsSelectionRefresh=!1),this._isNextRenderRedrawOnly||this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0},t.prototype.resize=function(e,t){this._rowCount=t,this._fireOnCanvasResize()},t.prototype.changeOptions=function(){this._renderer.onOptionsChanged(),this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize()},t.prototype._fireOnCanvasResize=function(){this._renderer.dimensions.canvasWidth===this._canvasWidth&&this._renderer.dimensions.canvasHeight===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.dimensions)},t.prototype.dispose=function(){e.prototype.dispose.call(this)},t.prototype.setRenderer=function(e){var t=this;this._renderer.dispose(),this._renderer=e,this._renderer.onRequestRedraw((function(e){return t.refreshRows(e.start,e.end,!0)})),this._needsSelectionRefresh=!0,this._fullRefresh()},t.prototype._fullRefresh=function(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)},t.prototype.setColors=function(e){this._renderer.setColors(e),this._fullRefresh()},t.prototype.onDevicePixelRatioChange=function(){this._renderer.onDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1)},t.prototype.onResize=function(e,t){this._renderer.onResize(e,t),this._fullRefresh()},t.prototype.onCharSizeChanged=function(){this._renderer.onCharSizeChanged()},t.prototype.onBlur=function(){this._renderer.onBlur()},t.prototype.onFocus=function(){this._renderer.onFocus()},t.prototype.onSelectionChanged=function(e,t,i){this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=i,this._renderer.onSelectionChanged(e,t,i)},t.prototype.onCursorMove=function(){this._renderer.onCursorMove()},t.prototype.clear=function(){this._renderer.clear()},t.prototype.registerCharacterJoiner=function(e){return this._renderer.registerCharacterJoiner(e)},t.prototype.deregisterCharacterJoiner=function(e){return this._renderer.deregisterCharacterJoiner(e)},s([o(3,d.IOptionsService),o(4,f.ICharSizeService),o(5,d.IBufferService)],t)}(c.Disposable);t.RenderService=p},function(e,t,i){"use strict";var n=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CharSizeService=void 0;var s=i(1),o=i(0),a=function(){function e(e,t,i){this._optionsService=i,this.width=0,this.height=0,this._onCharSizeChange=new o.EventEmitter,this._measureStrategy=new l(e,t,this._optionsService)}return Object.defineProperty(e.prototype,"hasValidSize",{get:function(){return this.width>0&&this.height>0},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onCharSizeChange",{get:function(){return this._onCharSizeChange.event},enumerable:!1,configurable:!0}),e.prototype.measure=function(){var e=this._measureStrategy.measure();e.width===this.width&&e.height===this.height||(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())},n([r(2,s.IOptionsService)],e)}();t.CharSizeService=a;var l=function(){function e(e,t,i){this._document=e,this._parentElement=t,this._optionsService=i,this._result={width:0,height:0},this._measureElement=this._document.createElement("span"),this._measureElement.classList.add("xterm-char-measure-element"),this._measureElement.textContent="W",this._measureElement.setAttribute("aria-hidden","true"),this._parentElement.appendChild(this._measureElement)}return e.prototype.measure=function(){this._measureElement.style.fontFamily=this._optionsService.options.fontFamily,this._measureElement.style.fontSize=this._optionsService.options.fontSize+"px";var e=this._measureElement.getBoundingClientRect();return 0!==e.width&&0!==e.height&&(this._result.width=e.width,this._result.height=Math.ceil(e.height)),this._result},e}()},function(e,t,i){"use strict";var n=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.MouseService=void 0;var s=i(5),o=i(30),a=function(){function e(e,t){this._renderService=e,this._charSizeService=t}return e.prototype.getCoords=function(e,t,i,n,r){return o.getCoords(e,t,i,n,this._charSizeService.hasValidSize,this._renderService.dimensions.actualCellWidth,this._renderService.dimensions.actualCellHeight,r)},e.prototype.getRawByteCoords=function(e,t,i,n){var r=this.getCoords(e,t,i,n);return o.getRawByteCoords(r)},n([r(0,s.IRenderService),r(1,s.ICharSizeService)],e)}();t.MouseService=a},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)}),s=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.Linkifier2=void 0;var a=i(1),l=i(0),c=i(2),h=i(7),u=function(e){function t(t){var i=e.call(this)||this;return i._bufferService=t,i._linkProviders=[],i._linkCacheDisposables=[],i._isMouseOut=!0,i._activeLine=-1,i._onShowLinkUnderline=i.register(new l.EventEmitter),i._onHideLinkUnderline=i.register(new l.EventEmitter),i.register(c.getDisposeArrayDisposable(i._linkCacheDisposables)),i}return r(t,e),Object.defineProperty(t.prototype,"onShowLinkUnderline",{get:function(){return this._onShowLinkUnderline.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onHideLinkUnderline",{get:function(){return this._onHideLinkUnderline.event},enumerable:!1,configurable:!0}),t.prototype.registerLinkProvider=function(e){var t=this;return this._linkProviders.push(e),{dispose:function(){var i=t._linkProviders.indexOf(e);-1!==i&&t._linkProviders.splice(i,1)}}},t.prototype.attachToDom=function(e,t,i){var n=this;this._element=e,this._mouseService=t,this._renderService=i,this.register(h.addDisposableDomListener(this._element,"mouseleave",(function(){n._isMouseOut=!0,n._clearCurrentLink()}))),this.register(h.addDisposableDomListener(this._element,"mousemove",this._onMouseMove.bind(this))),this.register(h.addDisposableDomListener(this._element,"click",this._onClick.bind(this)))},t.prototype._onMouseMove=function(e){if(this._lastMouseEvent=e,this._element&&this._mouseService){var t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(t){this._isMouseOut=!1;for(var i=e.composedPath(),n=0;ne?this._bufferService.cols:o.link.range.end.x,l=o.link.range.start.y=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,c.disposeArray(this._linkCacheDisposables))},t.prototype._handleNewLink=function(e){var t=this;if(this._element&&this._lastMouseEvent&&this._mouseService){var i=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);i&&this._linkAtPosition(e.link,i)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:void 0===e.link.decorations||e.link.decorations.underline,pointerCursor:void 0===e.link.decorations||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:function(){var e,i;return null===(i=null===(e=t._currentLink)||void 0===e?void 0:e.state)||void 0===i?void 0:i.decorations.pointerCursor},set:function(e){var i,n;(null===(i=t._currentLink)||void 0===i?void 0:i.state)&&t._currentLink.state.decorations.pointerCursor!==e&&(t._currentLink.state.decorations.pointerCursor=e,t._currentLink.state.isHovered&&(null===(n=t._element)||void 0===n||n.classList.toggle("xterm-cursor-pointer",e)))}},underline:{get:function(){var e,i;return null===(i=null===(e=t._currentLink)||void 0===e?void 0:e.state)||void 0===i?void 0:i.decorations.underline},set:function(i){var n,r,s;(null===(n=t._currentLink)||void 0===n?void 0:n.state)&&(null===(s=null===(r=t._currentLink)||void 0===r?void 0:r.state)||void 0===s?void 0:s.decorations.underline)!==i&&(t._currentLink.state.decorations.underline=i,t._currentLink.state.isHovered&&t._fireUnderlineEvent(e.link,i))}}}),this._renderService&&this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange((function(e){t._clearCurrentLink(0===e.start?0:e.start+1+t._bufferService.buffer.ydisp,e.end+1+t._bufferService.buffer.ydisp)}))))}},t.prototype._linkHover=function(e,t,i){var n;(null===(n=this._currentLink)||void 0===n?void 0:n.state)&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add("xterm-cursor-pointer")),t.hover&&t.hover(i,t.text)},t.prototype._fireUnderlineEvent=function(e,t){var i=e.range,n=this._bufferService.buffer.ydisp,r=this._createLinkUnderlineEvent(i.start.x-1,i.start.y-n-1,i.end.x,i.end.y-n-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(r)},t.prototype._linkLeave=function(e,t,i){var n;(null===(n=this._currentLink)||void 0===n?void 0:n.state)&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove("xterm-cursor-pointer")),t.leave&&t.leave(i,t.text)},t.prototype._linkAtPosition=function(e,t){var i=e.range.start.yt.y;return(e.range.start.y===e.range.end.y&&e.range.start.x<=t.x&&e.range.end.x>=t.x||i&&e.range.end.x>=t.x||n&&e.range.start.x<=t.x||i&&n)&&e.range.start.y<=t.y&&e.range.end.y>=t.y},t.prototype._positionFromMouseEvent=function(e,t,i){var n=i.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(n)return{x:n[0],y:n[1]+this._bufferService.buffer.ydisp}},t.prototype._createLinkUnderlineEvent=function(e,t,i,n,r){return{x1:e,y1:t,x2:i,y2:n,cols:this._bufferService.cols,fg:r}},s([o(0,a.IBufferService)],t)}(c.Disposable);t.Linkifier2=u},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CoreBrowserService=void 0;var n=function(){function e(e){this._textarea=e}return Object.defineProperty(e.prototype,"isFocused",{get:function(){return document.activeElement===this._textarea&&document.hasFocus()},enumerable:!1,configurable:!0}),e}();t.CoreBrowserService=n},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.CoreTerminal=void 0;var s=i(2),o=i(1),a=i(66),l=i(67),c=i(68),h=i(74),u=i(75),d=i(0),f=i(76),p=i(77),_=i(78),m=i(80),g=i(81),v=i(19),y=i(82),b=function(e){function t(t){var i=e.call(this)||this;return i._onBinary=new d.EventEmitter,i._onData=new d.EventEmitter,i._onLineFeed=new d.EventEmitter,i._onResize=new d.EventEmitter,i._onScroll=new d.EventEmitter,i._instantiationService=new a.InstantiationService,i.optionsService=new h.OptionsService(t),i._instantiationService.setService(o.IOptionsService,i.optionsService),i._bufferService=i.register(i._instantiationService.createInstance(c.BufferService)),i._instantiationService.setService(o.IBufferService,i._bufferService),i._logService=i._instantiationService.createInstance(l.LogService),i._instantiationService.setService(o.ILogService,i._logService),i._coreService=i.register(i._instantiationService.createInstance(u.CoreService,(function(){return i.scrollToBottom()}))),i._instantiationService.setService(o.ICoreService,i._coreService),i._coreMouseService=i._instantiationService.createInstance(f.CoreMouseService),i._instantiationService.setService(o.ICoreMouseService,i._coreMouseService),i._dirtyRowService=i._instantiationService.createInstance(p.DirtyRowService),i._instantiationService.setService(o.IDirtyRowService,i._dirtyRowService),i.unicodeService=i._instantiationService.createInstance(_.UnicodeService),i._instantiationService.setService(o.IUnicodeService,i.unicodeService),i._charsetService=i._instantiationService.createInstance(m.CharsetService),i._instantiationService.setService(o.ICharsetService,i._charsetService),i._inputHandler=new v.InputHandler(i._bufferService,i._charsetService,i._coreService,i._dirtyRowService,i._logService,i.optionsService,i._coreMouseService,i.unicodeService),i.register(d.forwardEvent(i._inputHandler.onLineFeed,i._onLineFeed)),i.register(i._inputHandler),i.register(d.forwardEvent(i._bufferService.onResize,i._onResize)),i.register(d.forwardEvent(i._coreService.onData,i._onData)),i.register(d.forwardEvent(i._coreService.onBinary,i._onBinary)),i.register(i.optionsService.onOptionChange((function(e){return i._updateOptions(e)}))),i._writeBuffer=new y.WriteBuffer((function(e){return i._inputHandler.parse(e)})),i}return r(t,e),Object.defineProperty(t.prototype,"onBinary",{get:function(){return this._onBinary.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onData",{get:function(){return this._onData.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onLineFeed",{get:function(){return this._onLineFeed.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onResize",{get:function(){return this._onResize.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onScroll",{get:function(){return this._onScroll.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"cols",{get:function(){return this._bufferService.cols},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"rows",{get:function(){return this._bufferService.rows},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"buffers",{get:function(){return this._bufferService.buffers},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){var t;this._isDisposed||(e.prototype.dispose.call(this),null===(t=this._windowsMode)||void 0===t||t.dispose(),this._windowsMode=void 0)},t.prototype.write=function(e,t){this._writeBuffer.write(e,t)},t.prototype.writeSync=function(e){this._writeBuffer.writeSync(e)},t.prototype.resize=function(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,c.MINIMUM_COLS),t=Math.max(t,c.MINIMUM_ROWS),this._bufferService.resize(e,t))},t.prototype.scroll=function(e,t){void 0===t&&(t=!1);var i,n=this._bufferService.buffer;(i=this._cachedBlankLine)&&i.length===this.cols&&i.getFg(0)===e.fg&&i.getBg(0)===e.bg||(i=n.getBlankLine(e,t),this._cachedBlankLine=i),i.isWrapped=t;var r=n.ybase+n.scrollTop,s=n.ybase+n.scrollBottom;if(0===n.scrollTop){var o=n.lines.isFull;s===n.lines.length-1?o?n.lines.recycle().copyFrom(i):n.lines.push(i.clone()):n.lines.splice(s+1,0,i.clone()),o?this._bufferService.isUserScrolling&&(n.ydisp=Math.max(n.ydisp-1,0)):(n.ybase++,this._bufferService.isUserScrolling||n.ydisp++)}else n.lines.shiftElements(r+1,s-r+1-1,-1),n.lines.set(s,i.clone());this._bufferService.isUserScrolling||(n.ydisp=n.ybase),this._dirtyRowService.markRangeDirty(n.scrollTop,n.scrollBottom),this._onScroll.fire(n.ydisp)},t.prototype.scrollLines=function(e,t){var i=this._bufferService.buffer;if(e<0){if(0===i.ydisp)return;this._bufferService.isUserScrolling=!0}else e+i.ydisp>=i.ybase&&(this._bufferService.isUserScrolling=!1);var n=i.ydisp;i.ydisp=Math.max(Math.min(i.ydisp+e,i.ybase),0),n!==i.ydisp&&(t||this._onScroll.fire(i.ydisp))},t.prototype.scrollPages=function(e){this.scrollLines(e*(this.rows-1))},t.prototype.scrollToTop=function(){this.scrollLines(-this._bufferService.buffer.ydisp)},t.prototype.scrollToBottom=function(){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)},t.prototype.scrollToLine=function(e){var t=e-this._bufferService.buffer.ydisp;0!==t&&this.scrollLines(t)},t.prototype.addEscHandler=function(e,t){return this._inputHandler.addEscHandler(e,t)},t.prototype.addDcsHandler=function(e,t){return this._inputHandler.addDcsHandler(e,t)},t.prototype.addCsiHandler=function(e,t){return this._inputHandler.addCsiHandler(e,t)},t.prototype.addOscHandler=function(e,t){return this._inputHandler.addOscHandler(e,t)},t.prototype._setup=function(){this.optionsService.options.windowsMode&&this._enableWindowsMode()},t.prototype.reset=function(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this._coreService.reset(),this._coreMouseService.reset()},t.prototype._updateOptions=function(e){var t;switch(e){case"scrollback":this.buffers.resize(this.cols,this.rows);break;case"windowsMode":this.optionsService.options.windowsMode?this._enableWindowsMode():(null===(t=this._windowsMode)||void 0===t||t.dispose(),this._windowsMode=void 0)}},t.prototype._enableWindowsMode=function(){var e=this;if(!this._windowsMode){var t=[];t.push(this.onLineFeed(g.updateWindowsModeWrappedState.bind(null,this._bufferService))),t.push(this.addCsiHandler({final:"H"},(function(){return g.updateWindowsModeWrappedState(e._bufferService),!1}))),this._windowsMode={dispose:function(){for(var e=0,i=t;e0?r[0].index:t.length;if(t.length!==u)throw new Error("[createInstance] First service dependency of "+e.name+" at position "+(u+1)+" conflicts with "+t.length+" static arguments");return new(e.bind.apply(e,n([void 0],n(t,o))))},e}();t.InstantiationService=a},function(e,t,i){"use strict";var n=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}},s=this&&this.__spreadArrays||function(){for(var e=0,t=0,i=arguments.length;t=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.BufferService=t.MINIMUM_ROWS=t.MINIMUM_COLS=void 0;var a=i(1),l=i(69),c=i(0),h=i(2);t.MINIMUM_COLS=2,t.MINIMUM_ROWS=1;var u=function(e){function i(i){var n=e.call(this)||this;return n._optionsService=i,n.isUserScrolling=!1,n._onResize=new c.EventEmitter,n.cols=Math.max(i.options.cols,t.MINIMUM_COLS),n.rows=Math.max(i.options.rows,t.MINIMUM_ROWS),n.buffers=new l.BufferSet(i,n),n}return r(i,e),Object.defineProperty(i.prototype,"onResize",{get:function(){return this._onResize.event},enumerable:!1,configurable:!0}),Object.defineProperty(i.prototype,"buffer",{get:function(){return this.buffers.active},enumerable:!1,configurable:!0}),i.prototype.dispose=function(){e.prototype.dispose.call(this),this.buffers.dispose()},i.prototype.resize=function(e,t){this.cols=e,this.rows=t,this.buffers.resize(e,t),this.buffers.setupTabStops(this.cols),this._onResize.fire({cols:e,rows:t})},i.prototype.reset=function(){this.buffers.dispose(),this.buffers=new l.BufferSet(this._optionsService,this),this.isUserScrolling=!1},s([o(0,a.IOptionsService)],i)}(h.Disposable);t.BufferService=u},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.BufferSet=void 0;var s=i(70),o=i(0),a=function(e){function t(t,i){var n=e.call(this)||this;return n._onBufferActivate=n.register(new o.EventEmitter),n._normal=new s.Buffer(!0,t,i),n._normal.fillViewportRows(),n._alt=new s.Buffer(!1,t,i),n._activeBuffer=n._normal,n.setupTabStops(),n}return r(t,e),Object.defineProperty(t.prototype,"onBufferActivate",{get:function(){return this._onBufferActivate.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"alt",{get:function(){return this._alt},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"active",{get:function(){return this._activeBuffer},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"normal",{get:function(){return this._normal},enumerable:!1,configurable:!0}),t.prototype.activateNormalBuffer=function(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))},t.prototype.activateAltBuffer=function(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))},t.prototype.resize=function(e,t){this._normal.resize(e,t),this._alt.resize(e,t)},t.prototype.setupTabStops=function(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)},t}(i(2).Disposable);t.BufferSet=a},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.BufferStringIterator=t.Buffer=t.MAX_BUFFER_SIZE=void 0;var n=i(71),r=i(16),s=i(4),o=i(3),a=i(72),l=i(73),c=i(20),h=i(6);t.MAX_BUFFER_SIZE=4294967295;var u=function(){function e(e,t,i){this._hasScrollback=e,this._optionsService=t,this._bufferService=i,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.savedY=0,this.savedX=0,this.savedCurAttrData=r.DEFAULT_ATTR_DATA.clone(),this.savedCharset=c.DEFAULT_CHARSET,this.markers=[],this._nullCell=s.CellData.fromCharData([0,o.NULL_CELL_CHAR,o.NULL_CELL_WIDTH,o.NULL_CELL_CODE]),this._whitespaceCell=s.CellData.fromCharData([0,o.WHITESPACE_CELL_CHAR,o.WHITESPACE_CELL_WIDTH,o.WHITESPACE_CELL_CODE]),this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new n.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}return e.prototype.getNullCell=function(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new h.ExtendedAttrs),this._nullCell},e.prototype.getWhitespaceCell=function(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new h.ExtendedAttrs),this._whitespaceCell},e.prototype.getBlankLine=function(e,t){return new r.BufferLine(this._bufferService.cols,this.getNullCell(e),t)},Object.defineProperty(e.prototype,"hasScrollback",{get:function(){return this._hasScrollback&&this.lines.maxLength>this._rows},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isCursorInViewport",{get:function(){var e=this.ybase+this.y-this.ydisp;return e>=0&&et.MAX_BUFFER_SIZE?t.MAX_BUFFER_SIZE:i},e.prototype.fillViewportRows=function(e){if(0===this.lines.length){void 0===e&&(e=r.DEFAULT_ATTR_DATA);for(var t=this._rows;t--;)this.lines.push(this.getBlankLine(e))}},e.prototype.clear=function(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new n.CircularList(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()},e.prototype.resize=function(e,t){var i=this.getNullCell(r.DEFAULT_ATTR_DATA),n=this._getCorrectBufferLength(t);if(n>this.lines.maxLength&&(this.lines.maxLength=n),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+o+1?(this.ybase--,o++,this.ydisp>0&&this.ydisp--):this.lines.push(new r.BufferLine(e,i)));else for(a=this._rows;a>t;a--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(n0&&(this.lines.trimStart(l),this.ybase=Math.max(this.ybase-l,0),this.ydisp=Math.max(this.ydisp-l,0),this.savedY=Math.max(this.savedY-l,0)),this.lines.maxLength=n}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),o&&(this.y+=o),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(s=0;sthis._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))},e.prototype._reflowLarger=function(e,t){var i=a.reflowLargerGetLinesToRemove(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(r.DEFAULT_ATTR_DATA));if(i.length>0){var n=a.reflowLargerCreateNewLayout(this.lines,i);a.reflowLargerApplyNewLayout(this.lines,n.layout),this._reflowLargerAdjustViewport(e,t,n.countRemoved)}},e.prototype._reflowLargerAdjustViewport=function(e,t,i){for(var n=this.getNullCell(r.DEFAULT_ATTR_DATA),s=i;s-- >0;)0===this.ybase?(this.y>0&&this.y--,this.lines.length=0;o--){var l=this.lines.get(o);if(!(!l||!l.isWrapped&&l.getTrimmedLength()<=e)){for(var c=[l];l.isWrapped&&o>0;)l=this.lines.get(--o),c.unshift(l);var h=this.ybase+this.y;if(!(h>=o&&h0&&(n.push({start:o+c.length+s,newLines:_}),s+=_.length),c.push.apply(c,_);var v=f.length-1,y=f[v];0===y&&(y=f[--v]);for(var b=c.length-p-1,C=d;b>=0;){var w=Math.min(C,y);if(c[v].copyCellsFrom(c[b],C-w,y-w,w,!0),0==(y-=w)&&(y=f[--v]),0==(C-=w)){b--;var S=Math.max(b,0);C=a.getWrappedLineTrimmedLength(c,S,this._cols)}}for(m=0;m0;)0===this.ybase?this.y0){var x=[],E=[];for(m=0;m=0;m--)if(R&&R.start>T+L){for(var D=R.newLines.length-1;D>=0;D--)this.lines.set(m--,R.newLines[D]);m++,x.push({index:T+1,amount:R.newLines.length}),L+=R.newLines.length,R=n[++O]}else this.lines.set(m,E[T--]);var P=0;for(m=x.length-1;m>=0;m--)x[m].index+=P,this.lines.onInsertEmitter.fire(x[m]),P+=x[m].amount;var I=Math.max(0,A+s-this.lines.maxLength);I>0&&this.lines.onTrimEmitter.fire(I)}},e.prototype.stringIndexToBufferIndex=function(e,t,i){for(void 0===i&&(i=!1);t;){var n=this.lines.get(e);if(!n)return[-1,-1];for(var r=i?n.getTrimmedLength():n.length,s=0;s0&&this.lines.get(t).isWrapped;)t--;for(;i+10;);return e>=this._cols?this._cols-1:e<0?0:e},e.prototype.nextStop=function(e){for(null==e&&(e=this.x);!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e},e.prototype.addMarker=function(e){var t=this,i=new l.Marker(e);return this.markers.push(i),i.register(this.lines.onTrim((function(e){i.line-=e,i.line<0&&i.dispose()}))),i.register(this.lines.onInsert((function(e){i.line>=e.index&&(i.line+=e.amount)}))),i.register(this.lines.onDelete((function(e){i.line>=e.index&&i.linee.index&&(i.line-=e.amount)}))),i.register(i.onDispose((function(){return t._removeMarker(i)}))),i},e.prototype._removeMarker=function(e){this.markers.splice(this.markers.indexOf(e),1)},e.prototype.iterator=function(e,t,i,n,r){return new d(this,e,t,i,n,r)},e}();t.Buffer=u;var d=function(){function e(e,t,i,n,r,s){void 0===i&&(i=0),void 0===n&&(n=e.lines.length),void 0===r&&(r=0),void 0===s&&(s=0),this._buffer=e,this._trimRight=t,this._startIndex=i,this._endIndex=n,this._startOverscan=r,this._endOverscan=s,this._startIndex<0&&(this._startIndex=0),this._endIndex>this._buffer.lines.length&&(this._endIndex=this._buffer.lines.length),this._current=this._startIndex}return e.prototype.hasNext=function(){return this._currentthis._endIndex+this._endOverscan&&(e.last=this._endIndex+this._endOverscan),e.first=Math.max(e.first,0),e.last=Math.min(e.last,this._buffer.lines.length);for(var t="",i=e.first;i<=e.last;++i)t+=this._buffer.translateBufferLineToString(i,this._trimRight);return this._current=e.last+1,{range:e,content:t}},e}();t.BufferStringIterator=d},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CircularList=void 0;var n=i(0),r=function(){function e(e){this._maxLength=e,this.onDeleteEmitter=new n.EventEmitter,this.onInsertEmitter=new n.EventEmitter,this.onTrimEmitter=new n.EventEmitter,this._array=new Array(this._maxLength),this._startIndex=0,this._length=0}return Object.defineProperty(e.prototype,"onDelete",{get:function(){return this.onDeleteEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onInsert",{get:function(){return this.onInsertEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"onTrim",{get:function(){return this.onTrimEmitter.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"maxLength",{get:function(){return this._maxLength},set:function(e){if(this._maxLength!==e){for(var t=new Array(e),i=0;ithis._length)for(var t=this._length;t=e;r--)this._array[this._getCyclicIndex(r+i.length)]=this._array[this._getCyclicIndex(r)];for(r=0;rthis._maxLength){var s=this._length+i.length-this._maxLength;this._startIndex+=s,this._length=this._maxLength,this.onTrimEmitter.fire(s)}else this._length+=i.length},e.prototype.trimStart=function(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)},e.prototype.shiftElements=function(e,t,i){if(!(t<=0)){if(e<0||e>=this._length)throw new Error("start argument out of range");if(e+i<0)throw new Error("Cannot shift elements in list beyond index 0");if(i>0){for(var n=t-1;n>=0;n--)this.set(e+n+i,this.get(e+n));var r=e+t+i-this._length;if(r>0)for(this._length+=r;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(n=0;n=a&&r0&&(v>u||0===h[v].getTrimmedLength());v--)g++;g>0&&(o.push(a+h.length-g),o.push(g)),a+=h.length-1}}}return o},t.reflowLargerCreateNewLayout=function(e,t){for(var i=[],n=0,r=t[n],s=0,o=0;oc&&(o-=c,a++);var h=2===e[a].getWidth(o-1);h&&o--;var u=h?i-1:i;r.push(u),l+=u}return r},t.getWrappedLineTrimmedLength=n},function(e,t,i){"use strict";var n,r=this&&this.__extends||(n=function(e,t){return(n=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i])})(e,t)},function(e,t){function i(){this.constructor=e}n(e,t),e.prototype=null===t?Object.create(t):(i.prototype=t.prototype,new i)});Object.defineProperty(t,"__esModule",{value:!0}),t.Marker=void 0;var s=i(0),o=function(e){function t(i){var n=e.call(this)||this;return n.line=i,n._id=t._nextId++,n.isDisposed=!1,n._onDispose=new s.EventEmitter,n}return r(t,e),Object.defineProperty(t.prototype,"id",{get:function(){return this._id},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onDispose",{get:function(){return this._onDispose.event},enumerable:!1,configurable:!0}),t.prototype.dispose=function(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire())},t._nextId=1,t}(i(2).Disposable);t.Marker=o},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.OptionsService=t.DEFAULT_OPTIONS=t.DEFAULT_BELL_SOUND=void 0;var n=i(0),r=i(11),s=i(33);t.DEFAULT_BELL_SOUND="data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",t.DEFAULT_OPTIONS=Object.freeze({cols:80,rows:24,cursorBlink:!1,cursorStyle:"block",cursorWidth:1,bellSound:t.DEFAULT_BELL_SOUND,bellStyle:"none",drawBoldTextInBrightColors:!0,fastScrollModifier:"alt",fastScrollSensitivity:5,fontFamily:"courier-new, courier, monospace",fontSize:15,fontWeight:"normal",fontWeightBold:"bold",lineHeight:1,linkTooltipHoverDuration:500,letterSpacing:0,logLevel:"info",scrollback:1e3,scrollSensitivity:1,screenReaderMode:!1,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!0,allowTransparency:!1,tabStopWidth:8,theme:{},rightClickSelectsWord:r.isMac,rendererType:"canvas",windowOptions:{},windowsMode:!1,wordSeparator:" ()[]{}',\"`",convertEol:!1,termName:"xterm",cancelEvents:!1});var o=["normal","bold","100","200","300","400","500","600","700","800","900"],a=["cols","rows"],l=function(){function e(e){this._onOptionChange=new n.EventEmitter,this.options=s.clone(t.DEFAULT_OPTIONS);for(var i=0,r=Object.keys(e);i=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},o=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreService=void 0;var a=i(1),l=i(0),c=i(33),h=i(2),u=Object.freeze({insertMode:!1}),d=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,origin:!1,reverseWraparound:!1,sendFocus:!1,wraparound:!0}),f=function(e){function t(t,i,n,r){var s=e.call(this)||this;return s._bufferService=i,s._logService=n,s._optionsService=r,s.isCursorInitialized=!1,s.isCursorHidden=!1,s._onData=s.register(new l.EventEmitter),s._onUserInput=s.register(new l.EventEmitter),s._onBinary=s.register(new l.EventEmitter),s._scrollToBottom=t,s.register({dispose:function(){return s._scrollToBottom=void 0}}),s.modes=c.clone(u),s.decPrivateModes=c.clone(d),s}return r(t,e),Object.defineProperty(t.prototype,"onData",{get:function(){return this._onData.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onUserInput",{get:function(){return this._onUserInput.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onBinary",{get:function(){return this._onBinary.event},enumerable:!1,configurable:!0}),t.prototype.reset=function(){this.modes=c.clone(u),this.decPrivateModes=c.clone(d)},t.prototype.triggerDataEvent=function(e,t){if(void 0===t&&(t=!1),!this._optionsService.options.disableStdin){var i=this._bufferService.buffer;i.ybase!==i.ydisp&&this._scrollToBottom(),t&&this._onUserInput.fire(),this._logService.debug('sending data "'+e+'"',(function(){return e.split("").map((function(e){return e.charCodeAt(0)}))})),this._onData.fire(e)}},t.prototype.triggerBinaryEvent=function(e){this._optionsService.options.disableStdin||(this._logService.debug('sending binary "'+e+'"',(function(){return e.split("").map((function(e){return e.charCodeAt(0)}))})),this._onBinary.fire(e))},s([o(1,a.IBufferService),o(2,a.ILogService),o(3,a.IOptionsService)],t)}(h.Disposable);t.CoreService=f},function(e,t,i){"use strict";var n=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.CoreMouseService=void 0;var s=i(1),o=i(0),a={NONE:{events:0,restrict:function(){return!1}},X10:{events:1,restrict:function(e){return 4!==e.button&&1===e.action&&(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)}},VT200:{events:19,restrict:function(e){return 32!==e.action}},DRAG:{events:23,restrict:function(e){return 32!==e.action||3!==e.button}},ANY:{events:31,restrict:function(e){return!0}}};function l(e,t){var i=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return 4===e.button?(i|=64,i|=e.action):(i|=3&e.button,4&e.button&&(i|=64),8&e.button&&(i|=128),32===e.action?i|=32:0!==e.action||t||(i|=3)),i}var c=String.fromCharCode,h={DEFAULT:function(e){var t=[l(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?"":"\x1b[M"+c(t[0])+c(t[1])+c(t[2])},SGR:function(e){var t=0===e.action&&4!==e.button?"m":"M";return"\x1b[<"+l(e,!0)+";"+e.col+";"+e.row+t}},u=function(){function e(e,t){this._bufferService=e,this._coreService=t,this._protocols={},this._encodings={},this._activeProtocol="",this._activeEncoding="",this._onProtocolChange=new o.EventEmitter,this._lastEvent=null;for(var i=0,n=Object.keys(a);i=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows)return!1;if(4===e.button&&32===e.action)return!1;if(3===e.button&&32!==e.action)return!1;if(4!==e.button&&(2===e.action||3===e.action))return!1;if(e.col++,e.row++,32===e.action&&this._lastEvent&&this._compareEvents(this._lastEvent,e))return!1;if(!this._protocols[this._activeProtocol].restrict(e))return!1;var t=this._encodings[this._activeEncoding](e);return t&&("DEFAULT"===this._activeEncoding?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0},e.prototype.explainEvents=function(e){return{down:!!(1&e),up:!!(2&e),drag:!!(4&e),move:!!(8&e),wheel:!!(16&e)}},e.prototype._compareEvents=function(e,t){return e.col===t.col&&e.row===t.row&&e.button===t.button&&e.action===t.action&&e.ctrl===t.ctrl&&e.alt===t.alt&&e.shift===t.shift},n([r(0,s.IBufferService),r(1,s.ICoreService)],e)}();t.CoreMouseService=u},function(e,t,i){"use strict";var n=this&&this.__decorate||function(e,t,i,n){var r,s=arguments.length,o=s<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,i):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)o=Reflect.decorate(e,t,i,n);else for(var a=e.length-1;a>=0;a--)(r=e[a])&&(o=(s<3?r(o):s>3?r(t,i,o):r(t,i))||o);return s>3&&o&&Object.defineProperty(t,i,o),o},r=this&&this.__param||function(e,t){return function(i,n){t(i,n,e)}};Object.defineProperty(t,"__esModule",{value:!0}),t.DirtyRowService=void 0;var s=i(1),o=function(){function e(e){this._bufferService=e,this.clearRange()}return Object.defineProperty(e.prototype,"start",{get:function(){return this._start},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"end",{get:function(){return this._end},enumerable:!1,configurable:!0}),e.prototype.clearRange=function(){this._start=this._bufferService.buffer.y,this._end=this._bufferService.buffer.y},e.prototype.markDirty=function(e){ethis._end&&(this._end=e)},e.prototype.markRangeDirty=function(e,t){if(e>t){var i=e;e=t,t=i}ethis._end&&(this._end=t)},e.prototype.markAllDirty=function(){this.markRangeDirty(0,this._bufferService.rows-1)},n([r(0,s.IBufferService)],e)}();t.DirtyRowService=o},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeService=void 0;var n=i(0),r=i(79),s=function(){function e(){this._providers=Object.create(null),this._active="",this._onChange=new n.EventEmitter;var e=new r.UnicodeV6;this.register(e),this._active=e.version,this._activeProvider=e}return Object.defineProperty(e.prototype,"onChange",{get:function(){return this._onChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"versions",{get:function(){return Object.keys(this._providers)},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"activeVersion",{get:function(){return this._active},set:function(e){if(!this._providers[e])throw new Error('unknown Unicode version "'+e+'"');this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)},enumerable:!1,configurable:!0}),e.prototype.register=function(e){this._providers[e.version]=e},e.prototype.wcwidth=function(e){return this._activeProvider.wcwidth(e)},e.prototype.getStringCellWidth=function(e){for(var t=0,i=e.length,n=0;n=i)return t+this.wcwidth(r);var s=e.charCodeAt(n);56320<=s&&s<=57343?r=1024*(r-55296)+s-56320+65536:t+=this.wcwidth(s)}t+=this.wcwidth(r)}return t},e}();t.UnicodeService=s},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.UnicodeV6=void 0;var n,r=i(15),s=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],o=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]],a=function(){function e(){if(this.version="6",!n){n=new Uint8Array(65536),r.fill(n,1),n[0]=0,r.fill(n,0,1,32),r.fill(n,0,127,160),r.fill(n,2,4352,4448),n[9001]=2,n[9002]=2,r.fill(n,2,11904,42192),n[12351]=1,r.fill(n,2,44032,55204),r.fill(n,2,63744,64256),r.fill(n,2,65040,65050),r.fill(n,2,65072,65136),r.fill(n,2,65280,65377),r.fill(n,2,65504,65511);for(var e=0;et[r][1])return!1;for(;r>=n;)if(e>t[i=n+r>>1][1])n=i+1;else{if(!(e=131072&&e<=196605||e>=196608&&e<=262141?2:1},e}();t.UnicodeV6=a},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CharsetService=void 0;var n=function(){function e(){this.glevel=0,this._charsets=[]}return e.prototype.reset=function(){this.charset=void 0,this._charsets=[],this.glevel=0},e.prototype.setgLevel=function(e){this.glevel=e,this.charset=this._charsets[e]},e.prototype.setgCharset=function(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)},e}();t.CharsetService=n},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.updateWindowsModeWrappedState=void 0;var n=i(3);t.updateWindowsModeWrappedState=function(e){var t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1),i=null==t?void 0:t.get(e.cols-1),r=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);r&&i&&(r.isWrapped=i[n.CHAR_DATA_CODE_INDEX]!==n.NULL_CELL_CODE&&i[n.CHAR_DATA_CODE_INDEX]!==n.WHITESPACE_CELL_CODE)}},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.WriteBuffer=void 0;var n=function(){function e(e){this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0}return e.prototype.writeSync=function(e){if(this._writeBuffer.length){for(var t=this._bufferOffset;t5e7)throw new Error("write data discarded, use flow control to avoid losing data");this._writeBuffer.length||(this._bufferOffset=0,setTimeout((function(){return i._innerWrite()}))),this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)},e.prototype._innerWrite=function(){for(var e=this,t=Date.now();this._writeBuffer.length>this._bufferOffset;){var i=this._writeBuffer[this._bufferOffset],n=this._callbacks[this._bufferOffset];if(this._bufferOffset++,this._action(i),this._pendingData-=i.length,n&&n(),Date.now()-t>=12)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>50&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout((function(){return e._innerWrite()}),0)):(this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0)},e}();t.WriteBuffer=n},function(e,t,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.AddonManager=void 0;var n=function(){function e(){this._addons=[]}return e.prototype.dispose=function(){for(var e=this._addons.length-1;e>=0;e--)this._addons[e].instance.dispose()},e.prototype.loadAddon=function(e,t){var i=this,n={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(n),t.dispose=function(){return i._wrappedAddonDispose(n)},t.activate(e)},e.prototype._wrappedAddonDispose=function(e){if(!e.isDisposed){for(var t=-1,i=0;i0&&(r.set(c,h-1),a++)}return 2*a/(e.length+t.length-2*(i-1))},t.default=t.stringSimilarity},zUnb:function(e,t,i){"use strict";function n(e){return"function"==typeof e}i.r(t);let r=!1;const s={Promise:void 0,set useDeprecatedSynchronousErrorHandling(e){if(e){const e=new Error;console.warn("DEPRECATED! RxJS was set to use deprecated synchronous error handling behavior by code at: \n"+e.stack)}else r&&console.log("RxJS: Back to a better error behavior. Thank you. <3");r=e},get useDeprecatedSynchronousErrorHandling(){return r}};function o(e){setTimeout(()=>{throw e},0)}const a={closed:!0,next(e){},error(e){if(s.useDeprecatedSynchronousErrorHandling)throw e;o(e)},complete(){}},l=(()=>Array.isArray||(e=>e&&"number"==typeof e.length))();function c(e){return null!==e&&"object"==typeof e}const h=(()=>{function e(e){return Error.call(this),this.message=e?`${e.length} errors occurred during unsubscription:\n${e.map((e,t)=>`${t+1}) ${e.toString()}`).join("\n ")}`:"",this.name="UnsubscriptionError",this.errors=e,this}return e.prototype=Object.create(Error.prototype),e})();let u=(()=>{class e{constructor(e){this.closed=!1,this._parentOrParents=null,this._subscriptions=null,e&&(this._unsubscribe=e)}unsubscribe(){let t;if(this.closed)return;let{_parentOrParents:i,_unsubscribe:r,_subscriptions:s}=this;if(this.closed=!0,this._parentOrParents=null,this._subscriptions=null,i instanceof e)i.remove(this);else if(null!==i)for(let e=0;ee.concat(t instanceof h?t.errors:t),[])}const f=(()=>"function"==typeof Symbol?Symbol("rxSubscriber"):"@@rxSubscriber_"+Math.random())();class p extends u{constructor(e,t,i){switch(super(),this.syncErrorValue=null,this.syncErrorThrown=!1,this.syncErrorThrowable=!1,this.isStopped=!1,arguments.length){case 0:this.destination=a;break;case 1:if(!e){this.destination=a;break}if("object"==typeof e){e instanceof p?(this.syncErrorThrowable=e.syncErrorThrowable,this.destination=e,e.add(this)):(this.syncErrorThrowable=!0,this.destination=new _(this,e));break}default:this.syncErrorThrowable=!0,this.destination=new _(this,e,t,i)}}[f](){return this}static create(e,t,i){const n=new p(e,t,i);return n.syncErrorThrowable=!1,n}next(e){this.isStopped||this._next(e)}error(e){this.isStopped||(this.isStopped=!0,this._error(e))}complete(){this.isStopped||(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe())}_next(e){this.destination.next(e)}_error(e){this.destination.error(e),this.unsubscribe()}_complete(){this.destination.complete(),this.unsubscribe()}_unsubscribeAndRecycle(){const{_parentOrParents:e}=this;return this._parentOrParents=null,this.unsubscribe(),this.closed=!1,this.isStopped=!1,this._parentOrParents=e,this}}class _ extends p{constructor(e,t,i,r){let s;super(),this._parentSubscriber=e;let o=this;n(t)?s=t:t&&(s=t.next,i=t.error,r=t.complete,t!==a&&(o=Object.create(t),n(o.unsubscribe)&&this.add(o.unsubscribe.bind(o)),o.unsubscribe=this.unsubscribe.bind(this))),this._context=o,this._next=s,this._error=i,this._complete=r}next(e){if(!this.isStopped&&this._next){const{_parentSubscriber:t}=this;s.useDeprecatedSynchronousErrorHandling&&t.syncErrorThrowable?this.__tryOrSetError(t,this._next,e)&&this.unsubscribe():this.__tryOrUnsub(this._next,e)}}error(e){if(!this.isStopped){const{_parentSubscriber:t}=this,{useDeprecatedSynchronousErrorHandling:i}=s;if(this._error)i&&t.syncErrorThrowable?(this.__tryOrSetError(t,this._error,e),this.unsubscribe()):(this.__tryOrUnsub(this._error,e),this.unsubscribe());else if(t.syncErrorThrowable)i?(t.syncErrorValue=e,t.syncErrorThrown=!0):o(e),this.unsubscribe();else{if(this.unsubscribe(),i)throw e;o(e)}}}complete(){if(!this.isStopped){const{_parentSubscriber:e}=this;if(this._complete){const t=()=>this._complete.call(this._context);s.useDeprecatedSynchronousErrorHandling&&e.syncErrorThrowable?(this.__tryOrSetError(e,t),this.unsubscribe()):(this.__tryOrUnsub(t),this.unsubscribe())}else this.unsubscribe()}}__tryOrUnsub(e,t){try{e.call(this._context,t)}catch(i){if(this.unsubscribe(),s.useDeprecatedSynchronousErrorHandling)throw i;o(i)}}__tryOrSetError(e,t,i){if(!s.useDeprecatedSynchronousErrorHandling)throw new Error("bad call");try{t.call(this._context,i)}catch(n){return s.useDeprecatedSynchronousErrorHandling?(e.syncErrorValue=n,e.syncErrorThrown=!0,!0):(o(n),!0)}return!1}_unsubscribe(){const{_parentSubscriber:e}=this;this._context=null,this._parentSubscriber=null,e.unsubscribe()}}const m=(()=>"function"==typeof Symbol&&Symbol.observable||"@@observable")();function g(e){return e}let v=(()=>{class e{constructor(e){this._isScalar=!1,e&&(this._subscribe=e)}lift(t){const i=new e;return i.source=this,i.operator=t,i}subscribe(e,t,i){const{operator:n}=this,r=function(e,t,i){if(e){if(e instanceof p)return e;if(e[f])return e[f]()}return e||t||i?new p(e,t,i):new p(a)}(e,t,i);if(r.add(n?n.call(r,this.source):this.source||s.useDeprecatedSynchronousErrorHandling&&!r.syncErrorThrowable?this._subscribe(r):this._trySubscribe(r)),s.useDeprecatedSynchronousErrorHandling&&r.syncErrorThrowable&&(r.syncErrorThrowable=!1,r.syncErrorThrown))throw r.syncErrorValue;return r}_trySubscribe(e){try{return this._subscribe(e)}catch(t){s.useDeprecatedSynchronousErrorHandling&&(e.syncErrorThrown=!0,e.syncErrorValue=t),function(e){for(;e;){const{closed:t,destination:i,isStopped:n}=e;if(t||n)return!1;e=i&&i instanceof p?i:null}return!0}(e)?e.error(t):console.warn(t)}}forEach(e,t){return new(t=y(t))((t,i)=>{let n;n=this.subscribe(t=>{try{e(t)}catch(r){i(r),n&&n.unsubscribe()}},i,t)})}_subscribe(e){const{source:t}=this;return t&&t.subscribe(e)}[m](){return this}pipe(...e){return 0===e.length?this:(0===(t=e).length?g:1===t.length?t[0]:function(e){return t.reduce((e,t)=>t(e),e)})(this);var t}toPromise(e){return new(e=y(e))((e,t)=>{let i;this.subscribe(e=>i=e,e=>t(e),()=>e(i))})}}return e.create=t=>new e(t),e})();function y(e){if(e||(e=s.Promise||Promise),!e)throw new Error("no Promise impl found");return e}const b=(()=>{function e(){return Error.call(this),this.message="object unsubscribed",this.name="ObjectUnsubscribedError",this}return e.prototype=Object.create(Error.prototype),e})();class C extends u{constructor(e,t){super(),this.subject=e,this.subscriber=t,this.closed=!1}unsubscribe(){if(this.closed)return;this.closed=!0;const e=this.subject,t=e.observers;if(this.subject=null,!t||0===t.length||e.isStopped||e.closed)return;const i=t.indexOf(this.subscriber);-1!==i&&t.splice(i,1)}}class w extends p{constructor(e){super(e),this.destination=e}}let S=(()=>{class e extends v{constructor(){super(),this.observers=[],this.closed=!1,this.isStopped=!1,this.hasError=!1,this.thrownError=null}[f](){return new w(this)}lift(e){const t=new k(this,this);return t.operator=e,t}next(e){if(this.closed)throw new b;if(!this.isStopped){const{observers:t}=this,i=t.length,n=t.slice();for(let r=0;rnew k(e,t),e})();class k extends S{constructor(e,t){super(),this.destination=e,this.source=t}next(e){const{destination:t}=this;t&&t.next&&t.next(e)}error(e){const{destination:t}=this;t&&t.error&&this.destination.error(e)}complete(){const{destination:e}=this;e&&e.complete&&this.destination.complete()}_subscribe(e){const{source:t}=this;return t?this.source.subscribe(e):u.EMPTY}}function x(e){return e&&"function"==typeof e.schedule}class E extends p{constructor(e,t,i){super(),this.parent=e,this.outerValue=t,this.outerIndex=i,this.index=0}_next(e){this.parent.notifyNext(this.outerValue,e,this.outerIndex,this.index++,this)}_error(e){this.parent.notifyError(e,this),this.unsubscribe()}_complete(){this.parent.notifyComplete(this),this.unsubscribe()}}const A=e=>t=>{for(let i=0,n=e.length;ie&&"number"==typeof e.length&&"function"!=typeof e;function L(e){return!!e&&"function"!=typeof e.subscribe&&"function"==typeof e.then}const D=e=>{if(e&&"function"==typeof e[m])return n=e,e=>{const t=n[m]();if("function"!=typeof t.subscribe)throw new TypeError("Provided object does not correctly implement Symbol.observable");return t.subscribe(e)};if(R(e))return A(e);if(L(e))return i=e,e=>(i.then(t=>{e.closed||(e.next(t),e.complete())},t=>e.error(t)).then(null,o),e);if(e&&"function"==typeof e[O])return t=e,e=>{const i=t[O]();for(;;){const t=i.next();if(t.done){e.complete();break}if(e.next(t.value),e.closed)break}return"function"==typeof i.return&&e.add(()=>{i.return&&i.return()}),e};{const t=c(e)?"an invalid object":`'${e}'`;throw new TypeError(`You provided ${t} where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.`)}var t,i,n};function P(e,t,i,n,r=new E(e,i,n)){if(!r.closed)return t instanceof v?t.subscribe(r):D(t)(r)}class I extends p{notifyNext(e,t,i,n,r){this.destination.next(t)}notifyError(e,t){this.destination.error(e)}notifyComplete(e){this.destination.complete()}}function M(e,t){return function(i){if("function"!=typeof e)throw new TypeError("argument is not a function. Are you looking for `mapTo()`?");return i.lift(new F(e,t))}}class F{constructor(e,t){this.project=e,this.thisArg=t}call(e,t){return t.subscribe(new H(e,this.project,this.thisArg))}}class H extends p{constructor(e,t,i){super(e),this.project=t,this.count=0,this.thisArg=i||this}_next(e){let t;try{t=this.project.call(this.thisArg,e,this.count++)}catch(i){return void this.destination.error(i)}this.destination.next(t)}}function B(e,t){return new v(i=>{const n=new u;let r=0;return n.add(t.schedule((function(){r!==e.length?(i.next(e[r++]),i.closed||n.add(this.schedule())):i.complete()}))),n})}function j(e,t){return t?function(e,t){if(null!=e){if(function(e){return e&&"function"==typeof e[m]}(e))return function(e,t){return new v(i=>{const n=new u;return n.add(t.schedule(()=>{const r=e[m]();n.add(r.subscribe({next(e){n.add(t.schedule(()=>i.next(e)))},error(e){n.add(t.schedule(()=>i.error(e)))},complete(){n.add(t.schedule(()=>i.complete()))}}))})),n})}(e,t);if(L(e))return function(e,t){return new v(i=>{const n=new u;return n.add(t.schedule(()=>e.then(e=>{n.add(t.schedule(()=>{i.next(e),n.add(t.schedule(()=>i.complete()))}))},e=>{n.add(t.schedule(()=>i.error(e)))}))),n})}(e,t);if(R(e))return B(e,t);if(function(e){return e&&"function"==typeof e[O]}(e)||"string"==typeof e)return function(e,t){if(!e)throw new Error("Iterable cannot be null");return new v(i=>{const n=new u;let r;return n.add(()=>{r&&"function"==typeof r.return&&r.return()}),n.add(t.schedule(()=>{r=e[O](),n.add(t.schedule((function(){if(i.closed)return;let e,t;try{const i=r.next();e=i.value,t=i.done}catch(n){return void i.error(n)}t?i.complete():(i.next(e),this.schedule())})))})),n})}(e,t)}throw new TypeError((null!==e&&typeof e||e)+" is not observable")}(e,t):e instanceof v?e:new v(D(e))}function N(e,t,i=Number.POSITIVE_INFINITY){return"function"==typeof t?n=>n.pipe(N((i,n)=>j(e(i,n)).pipe(M((e,r)=>t(i,e,n,r))),i)):("number"==typeof t&&(i=t),t=>t.lift(new V(e,i)))}class V{constructor(e,t=Number.POSITIVE_INFINITY){this.project=e,this.concurrent=t}call(e,t){return t.subscribe(new U(e,this.project,this.concurrent))}}class U extends I{constructor(e,t,i=Number.POSITIVE_INFINITY){super(e),this.project=t,this.concurrent=i,this.hasCompleted=!1,this.buffer=[],this.active=0,this.index=0}_next(e){this.active0?this._next(t.shift()):0===this.active&&this.hasCompleted&&this.destination.complete()}}function q(e=Number.POSITIVE_INFINITY){return N(g,e)}function z(e,t){return t?B(e,t):new v(A(e))}function W(...e){let t=Number.POSITIVE_INFINITY,i=null,n=e[e.length-1];return x(n)?(i=e.pop(),e.length>1&&"number"==typeof e[e.length-1]&&(t=e.pop())):"number"==typeof n&&(t=e.pop()),null===i&&1===e.length&&e[0]instanceof v?e[0]:q(t)(z(e,i))}function $(){return function(e){return e.lift(new K(e))}}class K{constructor(e){this.connectable=e}call(e,t){const{connectable:i}=this;i._refCount++;const n=new G(e,i),r=t.subscribe(n);return n.closed||(n.connection=i.connect()),r}}class G extends p{constructor(e,t){super(e),this.connectable=t}_unsubscribe(){const{connectable:e}=this;if(!e)return void(this.connection=null);this.connectable=null;const t=e._refCount;if(t<=0)return void(this.connection=null);if(e._refCount=t-1,t>1)return void(this.connection=null);const{connection:i}=this,n=e._connection;this.connection=null,!n||i&&n!==i||n.unsubscribe()}}class Z extends v{constructor(e,t){super(),this.source=e,this.subjectFactory=t,this._refCount=0,this._isComplete=!1}_subscribe(e){return this.getSubject().subscribe(e)}getSubject(){const e=this._subject;return e&&!e.isStopped||(this._subject=this.subjectFactory()),this._subject}connect(){let e=this._connection;return e||(this._isComplete=!1,e=this._connection=new u,e.add(this.source.subscribe(new X(this.getSubject(),this))),e.closed&&(this._connection=null,e=u.EMPTY)),e}refCount(){return $()(this)}}const Y=(()=>{const e=Z.prototype;return{operator:{value:null},_refCount:{value:0,writable:!0},_subject:{value:null,writable:!0},_connection:{value:null,writable:!0},_subscribe:{value:e._subscribe},_isComplete:{value:e._isComplete,writable:!0},getSubject:{value:e.getSubject},connect:{value:e.connect},refCount:{value:e.refCount}}})();class X extends w{constructor(e,t){super(e),this.connectable=t}_error(e){this._unsubscribe(),super._error(e)}_complete(){this.connectable._isComplete=!0,this._unsubscribe(),super._complete()}_unsubscribe(){const e=this.connectable;if(e){this.connectable=null;const t=e._connection;e._refCount=0,e._subject=null,e._connection=null,t&&t.unsubscribe()}}}function Q(e,t){return function(i){let n;if(n="function"==typeof e?e:function(){return e},"function"==typeof t)return i.lift(new J(n,t));const r=Object.create(i,Y);return r.source=i,r.subjectFactory=n,r}}class J{constructor(e,t){this.subjectFactory=e,this.selector=t}call(e,t){const{selector:i}=this,n=this.subjectFactory(),r=i(n).subscribe(e);return r.add(t.subscribe(n)),r}}function ee(){return new S}function te(){return e=>$()(Q(ee)(e))}function ie(e){for(let t in e)if(e[t]===ie)return t;throw Error("Could not find renamed property on target object.")}function ne(e,t){for(const i in t)t.hasOwnProperty(i)&&!e.hasOwnProperty(i)&&(e[i]=t[i])}function re(e){if("string"==typeof e)return e;if(Array.isArray(e))return"["+e.map(re).join(", ")+"]";if(null==e)return""+e;if(e.overriddenName)return""+e.overriddenName;if(e.name)return""+e.name;const t=e.toString();if(null==t)return""+t;const i=t.indexOf("\n");return-1===i?t:t.substring(0,i)}function se(e,t){return null==e||""===e?null===t?"":t:null==t||""===t?e:e+" "+t}const oe=ie({__forward_ref__:ie});function ae(e){return e.__forward_ref__=ae,e.toString=function(){return re(this())},e}function le(e){return ce(e)?e():e}function ce(e){return"function"==typeof e&&e.hasOwnProperty(oe)&&e.__forward_ref__===ae}class he extends Error{constructor(e,t){super(function(e,t){return`${e?`NG0${e}: `:""}${t}`}(e,t)),this.code=e}}function ue(e){return"string"==typeof e?e:null==e?"":String(e)}function de(e){return"function"==typeof e?e.name||e.toString():"object"==typeof e&&null!=e&&"function"==typeof e.type?e.type.name||e.type.toString():ue(e)}function fe(e,t){const i=t?" in "+t:"";throw new he("201",`No provider for ${de(e)} found${i}`)}function pe(e){return{token:e.token,providedIn:e.providedIn||null,factory:e.factory,value:void 0}}function _e(e){return{providers:e.providers||[],imports:e.imports||[]}}function me(e){return ge(e,ye)||ge(e,Ce)}function ge(e,t){return e.hasOwnProperty(t)?e[t]:null}function ve(e){return e&&(e.hasOwnProperty(be)||e.hasOwnProperty(we))?e[be]:null}const ye=ie({"\u0275prov":ie}),be=ie({"\u0275inj":ie}),Ce=ie({ngInjectableDef:ie}),we=ie({ngInjectorDef:ie});var Se=function(e){return e[e.Default=0]="Default",e[e.Host=1]="Host",e[e.Self=2]="Self",e[e.SkipSelf=4]="SkipSelf",e[e.Optional=8]="Optional",e}({});let ke;function xe(e){const t=ke;return ke=e,t}function Ee(e,t,i){const n=me(e);return n&&"root"==n.providedIn?void 0===n.value?n.value=n.factory():n.value:i&Se.Optional?null:void 0!==t?t:void fe(re(e),"Injector")}function Ae(e){return{toString:e}.toString()}var Te=function(e){return e[e.OnPush=0]="OnPush",e[e.Default=1]="Default",e}({}),Oe=function(e){return e[e.Emulated=0]="Emulated",e[e.None=2]="None",e[e.ShadowDom=3]="ShadowDom",e}({});const Re="undefined"!=typeof globalThis&&globalThis,Le="undefined"!=typeof window&&window,De="undefined"!=typeof self&&"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&self,Pe="undefined"!=typeof global&&global,Ie=Re||Pe||Le||De,Me={},Fe=[],He=[],Be=ie({"\u0275cmp":ie}),je=ie({"\u0275dir":ie}),Ne=ie({"\u0275pipe":ie}),Ve=ie({"\u0275mod":ie}),Ue=ie({"\u0275loc":ie}),qe=ie({"\u0275fac":ie}),ze=ie({__NG_ELEMENT_ID__:ie});let We=0;function $e(e){return Ae(()=>{const t={},i={type:e.type,providersResolver:null,decls:e.decls,vars:e.vars,factory:null,template:e.template||null,consts:e.consts||null,ngContentSelectors:e.ngContentSelectors,hostBindings:e.hostBindings||null,hostVars:e.hostVars||0,hostAttrs:e.hostAttrs||null,contentQueries:e.contentQueries||null,declaredInputs:t,inputs:null,outputs:null,exportAs:e.exportAs||null,onPush:e.changeDetection===Te.OnPush,directiveDefs:null,pipeDefs:null,selectors:e.selectors||He,viewQuery:e.viewQuery||null,features:e.features||null,data:e.data||{},encapsulation:e.encapsulation||Oe.Emulated,id:"c",styles:e.styles||He,_:null,setInput:null,schemas:e.schemas||null,tView:null},n=e.directives,r=e.features,s=e.pipes;return i.id+=We++,i.inputs=Xe(e.inputs,t),i.outputs=Xe(e.outputs),r&&r.forEach(e=>e(i)),i.directiveDefs=n?()=>("function"==typeof n?n():n).map(Ke):null,i.pipeDefs=s?()=>("function"==typeof s?s():s).map(Ge):null,i})}function Ke(e){return et(e)||function(e){return e[je]||null}(e)}function Ge(e){return function(e){return e[Ne]||null}(e)}const Ze={};function Ye(e){const t={type:e.type,bootstrap:e.bootstrap||He,declarations:e.declarations||He,imports:e.imports||He,exports:e.exports||He,transitiveCompileScopes:null,schemas:e.schemas||null,id:e.id||null};return null!=e.id&&Ae(()=>{Ze[e.id]=e.type}),t}function Xe(e,t){if(null==e)return Me;const i={};for(const n in e)if(e.hasOwnProperty(n)){let r=e[n],s=r;Array.isArray(r)&&(s=r[1],r=r[0]),i[r]=n,t&&(t[r]=s)}return i}const Qe=$e;function Je(e){return{type:e.type,name:e.name,factory:null,pure:!1!==e.pure,onDestroy:e.type.prototype.ngOnDestroy||null}}function et(e){return e[Be]||null}function tt(e,t){const i=e[Ve]||null;if(!i&&!0===t)throw new Error(`Type ${re(e)} does not have '\u0275mod' property.`);return i}const it=20,nt=10;function rt(e){return Array.isArray(e)&&"object"==typeof e[1]}function st(e){return Array.isArray(e)&&!0===e[1]}function ot(e){return 0!=(8&e.flags)}function at(e){return 2==(2&e.flags)}function lt(e){return 1==(1&e.flags)}function ct(e){return null!==e.template}function ht(e,t){return e.hasOwnProperty(qe)?e[qe]:null}class ut{constructor(e,t,i){this.previousValue=e,this.currentValue=t,this.firstChange=i}isFirstChange(){return this.firstChange}}function dt(){return ft}function ft(e){return e.type.prototype.ngOnChanges&&(e.setInput=_t),pt}function pt(){const e=mt(this),t=null==e?void 0:e.current;if(t){const i=e.previous;if(i===Me)e.previous=t;else for(let e in t)i[e]=t[e];e.current=null,this.ngOnChanges(t)}}function _t(e,t,i,n){const r=mt(e)||function(e,t){return e.__ngSimpleChanges__=t}(e,{previous:Me,current:null}),s=r.current||(r.current={}),o=r.previous,a=this.declaredInputs[i],l=o[a];s[a]=new ut(l&&l.currentValue,t,o===Me),e[n]=t}function mt(e){return e.__ngSimpleChanges__||null}dt.ngInherit=!0;const gt="http://www.w3.org/2000/svg";let vt=void 0;function yt(){return void 0!==vt?vt:"undefined"!=typeof document?document:void 0}function bt(e){return!!e.listen}const Ct={createRenderer:(e,t)=>yt()};function wt(e){for(;Array.isArray(e);)e=e[0];return e}function St(e,t){return wt(t[e])}function kt(e,t){return wt(t[e.index])}function xt(e,t){return e.data[t]}function Et(e,t){return e[t]}function At(e,t){const i=t[e];return rt(i)?i:i[0]}function Tt(e){const t=function(e){return e.__ngContext__||null}(e);return t?Array.isArray(t)?t:t.lView:null}function Ot(e){return 4==(4&e[2])}function Rt(e){return 128==(128&e[2])}function Lt(e,t){return null==t?null:e[t]}function Dt(e){e[18]=0}function Pt(e,t){e[5]+=t;let i=e,n=e[3];for(;null!==n&&(1===t&&1===i[5]||-1===t&&0===i[5]);)n[5]+=t,i=n,n=n[3]}const It={lFrame:ri(null),bindingsEnabled:!0,isInCheckNoChangesMode:!1};function Mt(){return It.bindingsEnabled}function Ft(){return It.lFrame.lView}function Ht(){return It.lFrame.tView}function Bt(e){It.lFrame.contextLView=e}function jt(){let e=Nt();for(;null!==e&&64===e.type;)e=e.parent;return e}function Nt(){return It.lFrame.currentTNode}function Vt(e,t){const i=It.lFrame;i.currentTNode=e,i.isParent=t}function Ut(){return It.lFrame.isParent}function qt(){It.lFrame.isParent=!1}function zt(){return It.isInCheckNoChangesMode}function Wt(e){It.isInCheckNoChangesMode=e}function $t(){const e=It.lFrame;let t=e.bindingRootIndex;return-1===t&&(t=e.bindingRootIndex=e.tView.bindingStartIndex),t}function Kt(){return It.lFrame.bindingIndex++}function Gt(e){const t=It.lFrame,i=t.bindingIndex;return t.bindingIndex=t.bindingIndex+e,i}function Zt(e,t){const i=It.lFrame;i.bindingIndex=i.bindingRootIndex=e,Yt(t)}function Yt(e){It.lFrame.currentDirectiveIndex=e}function Xt(e){const t=It.lFrame.currentDirectiveIndex;return-1===t?null:e[t]}function Qt(){return It.lFrame.currentQueryIndex}function Jt(e){It.lFrame.currentQueryIndex=e}function ei(e){const t=e[1];return 2===t.type?t.declTNode:1===t.type?e[6]:null}function ti(e,t,i){if(i&Se.SkipSelf){let n=t,r=e;for(;n=n.parent,!(null!==n||i&Se.Host||(n=ei(r),null===n)||(r=r[15],10&n.type)););if(null===n)return!1;t=n,e=r}const n=It.lFrame=ni();return n.currentTNode=t,n.lView=e,!0}function ii(e){const t=ni(),i=e[1];It.lFrame=t,t.currentTNode=i.firstChild,t.lView=e,t.tView=i,t.contextLView=e,t.bindingIndex=i.bindingStartIndex,t.inI18n=!1}function ni(){const e=It.lFrame,t=null===e?null:e.child;return null===t?ri(e):t}function ri(e){const t={currentTNode:null,isParent:!0,lView:null,tView:null,selectedIndex:-1,contextLView:null,elementDepthCount:0,currentNamespace:null,currentDirectiveIndex:-1,bindingRootIndex:-1,bindingIndex:-1,currentQueryIndex:0,parent:e,child:null,inI18n:!1};return null!==e&&(e.child=t),t}function si(){const e=It.lFrame;return It.lFrame=e.parent,e.currentTNode=null,e.lView=null,e}const oi=si;function ai(){const e=si();e.isParent=!0,e.tView=null,e.selectedIndex=-1,e.contextLView=null,e.elementDepthCount=0,e.currentDirectiveIndex=-1,e.currentNamespace=null,e.bindingRootIndex=-1,e.bindingIndex=-1,e.currentQueryIndex=0}function li(){return It.lFrame.selectedIndex}function ci(e){It.lFrame.selectedIndex=e}function hi(){const e=It.lFrame;return xt(e.tView,e.selectedIndex)}function ui(){It.lFrame.currentNamespace=gt}function di(e,t){for(let i=t.directiveStart,n=t.directiveEnd;i=n)break}else t[a]<0&&(e[18]+=65536),(o>11>16&&(3&e[2])===t&&(e[2]+=2048,s.call(o)):s.call(o)}const vi=-1;class yi{constructor(e,t,i){this.factory=e,this.resolving=!1,this.canSeeViewProviders=t,this.injectImpl=i}}function bi(e,t,i){const n=bt(e);let r=0;for(;rt){o=s-1;break}}}for(;s>16,n=t;for(;i>0;)n=n[15],i--;return n}let Ti=!0;function Oi(e){const t=Ti;return Ti=e,t}let Ri=0;function Li(e,t){const i=Pi(e,t);if(-1!==i)return i;const n=t[1];n.firstCreatePass&&(e.injectorIndex=t.length,Di(n.data,e),Di(t,null),Di(n.blueprint,null));const r=Ii(e,t),s=e.injectorIndex;if(xi(r)){const e=Ei(r),i=Ai(r,t),n=i[1].data;for(let r=0;r<8;r++)t[s+r]=i[e+r]|n[e+r]}return t[s+8]=r,s}function Di(e,t){e.push(0,0,0,0,0,0,0,0,t)}function Pi(e,t){return-1===e.injectorIndex||e.parent&&e.parent.injectorIndex===e.injectorIndex||null===t[e.injectorIndex+8]?-1:e.injectorIndex}function Ii(e,t){if(e.parent&&-1!==e.parent.injectorIndex)return e.parent.injectorIndex;let i=0,n=null,r=t;for(;null!==r;){const e=r[1],t=e.type;if(n=2===t?e.declTNode:1===t?r[6]:null,null===n)return vi;if(i++,r=r[15],-1!==n.injectorIndex)return n.injectorIndex|i<<16}return vi}function Mi(e,t,i){!function(e,t,i){let n;"string"==typeof i?n=i.charCodeAt(0)||0:i.hasOwnProperty(ze)&&(n=i[ze]),null==n&&(n=i[ze]=Ri++);const r=255&n;t.data[e+(r>>5)]|=1<=0?255&t:Ni:t}(i);if("function"==typeof s){if(!ti(t,e,n))return n&Se.Host?Fi(r,i,n):Hi(t,i,n,r);try{const e=s();if(null!=e||n&Se.Optional)return e;fe(i)}finally{oi()}}else if("number"==typeof s){let r=null,o=Pi(e,t),a=vi,l=n&Se.Host?t[16][6]:null;for((-1===o||n&Se.SkipSelf)&&(a=-1===o?Ii(e,t):t[o+8],a!==vi&&Wi(n,!1)?(r=t[1],o=Ei(a),t=Ai(a,t)):o=-1);-1!==o;){const e=t[1];if(zi(s,o,e.data)){const e=Vi(o,t,i,r,n,l);if(e!==ji)return e}a=t[o+8],a!==vi&&Wi(n,t[1].data[o+8]===l)&&zi(s,o,t)?(r=e,o=Ei(a),t=Ai(a,t)):o=-1}}}return Hi(t,i,n,r)}const ji={};function Ni(){return new $i(jt(),Ft())}function Vi(e,t,i,n,r,s){const o=t[1],a=o.data[e+8],l=Ui(a,o,i,null==n?at(a)&&Ti:n!=o&&0!=(3&a.type),r&Se.Host&&s===a);return null!==l?qi(t,o,l,a):ji}function Ui(e,t,i,n,r){const s=e.providerIndexes,o=t.data,a=1048575&s,l=e.directiveStart,c=s>>20,h=r?a+c:e.directiveEnd;for(let u=n?a:a+c;u=l&&e.type===i)return u}if(r){const e=o[l];if(e&&ct(e)&&e.type===i)return l}return null}function qi(e,t,i,n){let r=e[i];const s=t.data;if(r instanceof yi){const o=r;o.resolving&&function(e,t){throw new he("200","Circular dependency in DI detected for "+e)}(de(s[i]));const a=Oi(o.canSeeViewProviders);o.resolving=!0;const l=o.injectImpl?xe(o.injectImpl):null;ti(e,n,Se.Default);try{r=e[i]=o.factory(void 0,s,e,n),t.firstCreatePass&&i>=n.directiveStart&&function(e,t,i){const{ngOnChanges:n,ngOnInit:r,ngDoCheck:s}=t.type.prototype;if(n){const n=ft(t);(i.preOrderHooks||(i.preOrderHooks=[])).push(e,n),(i.preOrderCheckHooks||(i.preOrderCheckHooks=[])).push(e,n)}r&&(i.preOrderHooks||(i.preOrderHooks=[])).push(0-e,r),s&&((i.preOrderHooks||(i.preOrderHooks=[])).push(e,s),(i.preOrderCheckHooks||(i.preOrderCheckHooks=[])).push(e,s))}(i,s[i],t)}finally{null!==l&&xe(l),Oi(a),o.resolving=!1,oi()}}return r}function zi(e,t,i){return!!(i[t+(e>>5)]&1<{const t=e.prototype.constructor,i=t[qe]||Gi(t),n=Object.prototype;let r=Object.getPrototypeOf(e.prototype).constructor;for(;r&&r!==n;){const e=r[qe]||Gi(r);if(e&&e!==i)return e;r=Object.getPrototypeOf(r)}return e=>new e})}function Gi(e){return ce(e)?()=>{const t=Gi(le(e));return t&&t()}:ht(e)}function Zi(e){return function(e,t){if("class"===t)return e.classes;if("style"===t)return e.styles;const i=e.attrs;if(i){const e=i.length;let n=0;for(;n{const n=function(e){return function(...t){if(e){const i=e(...t);for(const e in i)this[e]=i[e]}}}(t);function r(...e){if(this instanceof r)return n.apply(this,e),this;const t=new r(...e);return i.annotation=t,i;function i(e,i,n){const r=e.hasOwnProperty(Yi)?e[Yi]:Object.defineProperty(e,Yi,{value:[]})[Yi];for(;r.length<=n;)r.push(null);return(r[n]=r[n]||[]).push(t),e}}return i&&(r.prototype=Object.create(i.prototype)),r.prototype.ngMetadataName=e,r.annotationCls=r,r})}class Qi{constructor(e,t){this._desc=e,this.ngMetadataName="InjectionToken",this.\u0275prov=void 0,"number"==typeof t?this.__NG_ELEMENT_ID__=t:void 0!==t&&(this.\u0275prov=pe({token:this,providedIn:t.providedIn||"root",factory:t.factory}))}toString(){return"InjectionToken "+this._desc}}const Ji=new Qi("AnalyzeForEntryComponents"),en=Function;function tn(e,t){e.forEach(e=>Array.isArray(e)?tn(e,t):t(e))}function nn(e,t,i){t>=e.length?e.push(i):e.splice(t,0,i)}function rn(e,t){return t>=e.length-1?e.pop():e.splice(t,1)[0]}function sn(e,t){const i=[];for(let n=0;n=0?e[1|n]=i:(n=~n,function(e,t,i,n){let r=e.length;if(r==t)e.push(i,n);else if(1===r)e.push(n,e[0]),e[0]=i;else{for(r--,e.push(e[r-1],e[r]);r>t;)e[r]=e[r-2],r--;e[t]=i,e[t+1]=n}}(e,n,t,i)),n}function an(e,t){const i=ln(e,t);if(i>=0)return e[1|i]}function ln(e,t){return function(e,t,i){let n=0,r=e.length>>1;for(;r!==n;){const i=n+(r-n>>1),s=e[i<<1];if(t===s)return i<<1;s>t?r=i:n=i+1}return~(r<<1)}(e,t)}const cn={},hn=/\n/gm,un="__source",dn=ie({provide:String,useValue:ie});let fn=void 0;function pn(e){const t=fn;return fn=e,t}function _n(e,t=Se.Default){if(void 0===fn)throw new Error("inject() must be called from an injection context");return null===fn?Ee(e,void 0,t):fn.get(e,t&Se.Optional?null:void 0,t)}function mn(e,t=Se.Default){return(ke||_n)(le(e),t)}const gn=mn;function vn(e){const t=[];for(let i=0;i({token:e})),-1),Cn=yn(Xi("Optional"),8),wn=yn(Xi("SkipSelf"),4);let Sn,kn;function xn(e){var t;return(null===(t=function(){if(void 0===Sn&&(Sn=null,Ie.trustedTypes))try{Sn=Ie.trustedTypes.createPolicy("angular",{createHTML:e=>e,createScript:e=>e,createScriptURL:e=>e})}catch(t){}return Sn}())||void 0===t?void 0:t.createHTML(e))||e}function En(e){var t;return(null===(t=function(){if(void 0===kn&&(kn=null,Ie.trustedTypes))try{kn=Ie.trustedTypes.createPolicy("angular#unsafe-bypass",{createHTML:e=>e,createScript:e=>e,createScriptURL:e=>e})}catch(t){}return kn}())||void 0===t?void 0:t.createHTML(e))||e}class An{constructor(e){this.changingThisBreaksApplicationSecurity=e}toString(){return"SafeValue must use [property]=binding: "+this.changingThisBreaksApplicationSecurity+" (see https://g.co/ng/security#xss)"}}function Tn(e){return e instanceof An?e.changingThisBreaksApplicationSecurity:e}function On(e,t){const i=function(e){return e instanceof An&&e.getTypeName()||null}(e);if(null!=i&&i!==t){if("ResourceURL"===i&&"URL"===t)return!0;throw new Error(`Required a safe ${t}, got a ${i} (see https://g.co/ng/security#xss)`)}return i===t}class Rn{constructor(e){this.inertDocumentHelper=e}getInertBodyElement(e){e=" "+e;try{const t=(new window.DOMParser).parseFromString(xn(e),"text/html").body;return null===t?this.inertDocumentHelper.getInertBodyElement(e):(t.removeChild(t.firstChild),t)}catch(t){return null}}}class Ln{constructor(e){if(this.defaultDoc=e,this.inertDocument=this.defaultDoc.implementation.createHTMLDocument("sanitization-inert"),null==this.inertDocument.body){const e=this.inertDocument.createElement("html");this.inertDocument.appendChild(e);const t=this.inertDocument.createElement("body");e.appendChild(t)}}getInertBodyElement(e){const t=this.inertDocument.createElement("template");if("content"in t)return t.innerHTML=xn(e),t;const i=this.inertDocument.createElement("body");return i.innerHTML=xn(e),this.defaultDoc.documentMode&&this.stripCustomNsAttrs(i),i}stripCustomNsAttrs(e){const t=e.attributes;for(let n=t.length-1;0In(e.trim())).join(", ")),this.buf.push(" ",t,'="',Zn(o),'"')}var n;return this.buf.push(">"),!0}endElement(e){const t=e.nodeName.toLowerCase();Vn.hasOwnProperty(t)&&!Hn.hasOwnProperty(t)&&(this.buf.push(""),this.buf.push(t),this.buf.push(">"))}chars(e){this.buf.push(Zn(e))}checkClobberedElement(e,t){if(t&&(e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY)===Node.DOCUMENT_POSITION_CONTAINED_BY)throw new Error("Failed to sanitize html because the element is clobbered: "+e.outerHTML);return t}}const Kn=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Gn=/([^\#-~ |!])/g;function Zn(e){return e.replace(/&/g,"&").replace(Kn,(function(e){return""+(1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320)+65536)+";"})).replace(Gn,(function(e){return""+e.charCodeAt(0)+";"})).replace(//g,">")}let Yn;function Xn(e){return"content"in e&&function(e){return e.nodeType===Node.ELEMENT_NODE&&"TEMPLATE"===e.nodeName}(e)?e.content:null}var Qn=function(e){return e[e.NONE=0]="NONE",e[e.HTML=1]="HTML",e[e.STYLE=2]="STYLE",e[e.SCRIPT=3]="SCRIPT",e[e.URL=4]="URL",e[e.RESOURCE_URL=5]="RESOURCE_URL",e}({});function Jn(e){const t=tr();return t?En(t.sanitize(Qn.HTML,e)||""):On(e,"HTML")?En(Tn(e)):function(e,t){let i=null;try{Yn=Yn||function(e){const t=new Ln(e);return function(){try{return!!(new window.DOMParser).parseFromString(xn(""),"text/html")}catch(e){return!1}}()?new Rn(t):t}(e);let n=t?String(t):"";i=Yn.getInertBodyElement(n);let r=5,s=n;do{if(0===r)throw new Error("Failed to sanitize html because the input is unstable");r--,n=s,s=i.innerHTML,i=Yn.getInertBodyElement(n)}while(n!==s);return xn((new $n).sanitizeChildren(Xn(i)||i))}finally{if(i){const e=Xn(i)||i;for(;e.firstChild;)e.removeChild(e.firstChild)}}}(yt(),ue(e))}function er(e){const t=tr();return t?t.sanitize(Qn.URL,e)||"":On(e,"URL")?Tn(e):In(ue(e))}function tr(){const e=Ft();return e&&e[12]}function ir(e){return e.ngDebugContext}function nr(e){return e.ngOriginalError}function rr(e,...t){e.error(...t)}class sr{constructor(){this._console=console}handleError(e){const t=this._findOriginalError(e),i=this._findContext(e),n=function(e){return e.ngErrorLogger||rr}(e);n(this._console,"ERROR",e),t&&n(this._console,"ORIGINAL ERROR",t),i&&n(this._console,"ERROR CONTEXT",i)}_findContext(e){return e?ir(e)?ir(e):this._findContext(nr(e)):null}_findOriginalError(e){let t=nr(e);for(;t&&nr(t);)t=nr(t);return t}}function or(e,t){e.__ngContext__=t}const ar=(()=>("undefined"!=typeof requestAnimationFrame&&requestAnimationFrame||setTimeout).bind(Ie))();function lr(e){return{name:"window",target:e.ownerDocument.defaultView}}function cr(e){return e instanceof Function?e():e}var hr=function(e){return e[e.Important=1]="Important",e[e.DashCase=2]="DashCase",e}({});function ur(e,t){return(void 0)(e,t)}function dr(e){const t=e[3];return st(t)?t[3]:t}function fr(e){return _r(e[13])}function pr(e){return _r(e[4])}function _r(e){for(;null!==e&&!st(e);)e=e[4];return e}function mr(e,t,i,n,r){if(null!=n){let s,o=!1;st(n)?s=n:rt(n)&&(o=!0,n=n[0]);const a=wt(n);0===e&&null!==i?null==r?kr(t,i,a):Sr(t,i,a,r||null,!0):1===e&&null!==i?Sr(t,i,a,r||null,!0):2===e?function(e,t,i){const n=Er(e,t);n&&function(e,t,i,n){bt(e)?e.removeChild(t,i,n):t.removeChild(i)}(e,n,t,i)}(t,a,o):3===e&&t.destroyNode(a),null!=s&&function(e,t,i,n,r){const s=i[7];s!==wt(i)&&mr(t,e,n,s,r);for(let o=nt;o0&&(e[i-1][4]=n[4]);const o=rn(e,nt+t);Dr(n[1],r=n,r[11],2,null,null),r[0]=null,r[6]=null;const a=o[19];null!==a&&a.detachView(o[1]),n[3]=null,n[4]=null,n[2]&=-129}var r;return n}function br(e,t){if(!(256&t[2])){const i=t[11];bt(i)&&i.destroyNode&&Dr(e,t,i,3,null,null),function(e){let t=e[13];if(!t)return Cr(e[1],e);for(;t;){let i=null;if(rt(t))i=t[13];else{const e=t[10];e&&(i=e)}if(!i){for(;t&&!t[4]&&t!==e;)rt(t)&&Cr(t[1],t),t=t[3];null===t&&(t=e),rt(t)&&Cr(t[1],t),i=t&&t[4]}t=i}}(t)}}function Cr(e,t){if(!(256&t[2])){t[2]&=-129,t[2]|=256,function(e,t){let i;if(null!=e&&null!=(i=e.destroyHooks))for(let n=0;n=0?n[r=l]():n[r=-l].unsubscribe(),s+=2}else{const e=n[r=i[s+1]];i[s].call(e)}if(null!==n){for(let e=r+1;es?"":r[h+1].toLowerCase();const t=8&n?e:null;if(t&&-1!==Fr(t,c,0)||2&n&&c!==e){if(Ur(n))return!1;o=!0}}}}else{if(!o&&!Ur(n)&&!Ur(l))return!1;if(o&&Ur(l))continue;o=!1,n=l|1&n}}return Ur(n)||o}function Ur(e){return 0==(1&e)}function qr(e,t,i,n){if(null===t)return-1;let r=0;if(n||!i){let i=!1;for(;r-1)for(i++;i0?'="'+t+'"':"")+"]"}else 8&n?r+="."+o:4&n&&(r+=" "+o);else""===r||Ur(o)||(t+=$r(s,r),r=""),n=o,s=s||!Ur(n);i++}return""!==r&&(t+=$r(s,r)),t}const Gr={};function Zr(e){Yr(Ht(),Ft(),li()+e,zt())}function Yr(e,t,i,n){if(!n)if(3==(3&t[2])){const n=e.preOrderCheckHooks;null!==n&&fi(t,n,i)}else{const n=e.preOrderHooks;null!==n&&pi(t,n,0,i)}ci(i)}function Xr(e,t){return e<<17|t<<2}function Qr(e){return e>>17&32767}function Jr(e){return 2|e}function es(e){return(131068&e)>>2}function ts(e,t){return-131069&e|t<<2}function is(e){return 1|e}function ns(e,t){const i=e.contentQueries;if(null!==i)for(let n=0;nit&&Yr(e,t,it,zt()),i(n,r)}finally{ci(s)}}function us(e,t,i){if(ot(t)){const n=t.directiveEnd;for(let r=t.directiveStart;r0;){const i=e[--t];if("number"==typeof i&&i<0)return i}return 0})(i)!=s&&i.push(s),i.push(n,r,o)}}function Cs(e,t){null!==e.hostBindings&&e.hostBindings(1,t)}function ws(e,t){t.flags|=2,(e.components||(e.components=[])).push(t.index)}function Ss(e,t,i){if(i){if(t.exportAs)for(let n=0;n0&&function e(t){for(let n=fr(t);null!==n;n=pr(n))for(let t=nt;t0&&e(i)}const i=t[1].components;if(null!==i)for(let n=0;n0&&e(r)}}(i)}}function Ls(e,t){const i=At(t,e),n=i[1];!function(e,t){for(let i=t.length;iPromise.resolve(null))();function Bs(e){return e[7]||(e[7]=[])}function js(e){return e.cleanup||(e.cleanup=[])}function Ns(e,t,i){return(null===e||ct(e))&&(i=function(e){for(;Array.isArray(e);){if("object"==typeof e[1])return e;e=e[0]}return null}(i[t.index])),i[11]}function Vs(e,t){const i=e[9],n=i?i.get(sr,null):null;n&&n.handleError(t)}function Us(e,t,i,n,r){for(let s=0;sthis.processProvider(i,e,t)),tn([e],e=>this.processInjectorType(e,[],r)),this.records.set(zs,io(void 0,this));const s=this.records.get($s);this.scope=null!=s?s.value:null,this.source=n||("object"==typeof e?null:re(e))}get destroyed(){return this._destroyed}destroy(){this.assertNotDestroyed(),this._destroyed=!0;try{this.onDestroy.forEach(e=>e.ngOnDestroy())}finally{this.records.clear(),this.onDestroy.clear(),this.injectorDefTypes.clear()}}get(e,t=cn,i=Se.Default){this.assertNotDestroyed();const n=pn(this);try{if(!(i&Se.SkipSelf)){let t=this.records.get(e);if(void 0===t){const i=("function"==typeof(r=e)||"object"==typeof r&&r instanceof Qi)&&me(e);t=i&&this.injectableDefInScope(i)?io(eo(e),Ks):null,this.records.set(e,t)}if(null!=t)return this.hydrate(e,t)}return(i&Se.Self?Xs():this.parent).get(e,t=i&Se.Optional&&t===cn?null:t)}catch(s){if("NullInjectorError"===s.name){if((s.ngTempTokenPath=s.ngTempTokenPath||[]).unshift(re(e)),n)throw s;return function(e,t,i,n){const r=e.ngTempTokenPath;throw t[un]&&r.unshift(t[un]),e.message=function(e,t,i,n=null){e=e&&"\n"===e.charAt(0)&&"\u0275"==e.charAt(1)?e.substr(2):e;let r=re(t);if(Array.isArray(t))r=t.map(re).join(" -> ");else if("object"==typeof t){let e=[];for(let i in t)if(t.hasOwnProperty(i)){let n=t[i];e.push(i+":"+("string"==typeof n?JSON.stringify(n):re(n)))}r=`{${e.join(", ")}}`}return`${i}${n?"("+n+")":""}[${r}]: ${e.replace(hn,"\n ")}`}("\n"+e.message,r,i,n),e.ngTokenPath=r,e.ngTempTokenPath=null,e}(s,e,"R3InjectorError",this.source)}throw s}finally{pn(n)}var r}_resolveInjectorDefTypes(){this.injectorDefTypes.forEach(e=>this.get(e))}toString(){const e=[];return this.records.forEach((t,i)=>e.push(re(i))),`R3Injector[${e.join(", ")}]`}assertNotDestroyed(){if(this._destroyed)throw new Error("Injector has already been destroyed.")}processInjectorType(e,t,i){if(!(e=le(e)))return!1;let n=ve(e);const r=null==n&&e.ngModule||void 0,s=void 0===r?e:r,o=-1!==i.indexOf(s);if(void 0!==r&&(n=ve(r)),null==n)return!1;if(null!=n.imports&&!o){let e;i.push(s);try{tn(n.imports,n=>{this.processInjectorType(n,t,i)&&(void 0===e&&(e=[]),e.push(n))})}finally{}if(void 0!==e)for(let t=0;tthis.processProvider(e,i,n||Zs))}}this.injectorDefTypes.add(s);const a=ht(s)||(()=>new s);this.records.set(s,io(a,Ks));const l=n.providers;if(null!=l&&!o){const t=e;tn(l,e=>this.processProvider(e,t,l))}return void 0!==r&&void 0!==e.providers}processProvider(e,t,i){let n=ro(e=le(e))?e:le(e&&e.provide);const r=function(e,t,i){return no(e)?io(void 0,e.useValue):io(to(e),Ks)}(e);if(ro(e)||!0!==e.multi)this.records.get(n);else{let t=this.records.get(n);t||(t=io(void 0,Ks,!0),t.factory=()=>vn(t.multi),this.records.set(n,t)),n=e,t.multi.push(e)}this.records.set(n,r)}hydrate(e,t){var i;return t.value===Ks&&(t.value=Gs,t.value=t.factory()),"object"==typeof t.value&&t.value&&null!==(i=t.value)&&"object"==typeof i&&"function"==typeof i.ngOnDestroy&&this.onDestroy.add(t.value),t.value}injectableDefInScope(e){return!!e.providedIn&&("string"==typeof e.providedIn?"any"===e.providedIn||e.providedIn===this.scope:this.injectorDefTypes.has(e.providedIn))}}function eo(e){const t=me(e),i=null!==t?t.factory:ht(e);if(null!==i)return i;if(e instanceof Qi)throw new Error(`Token ${re(e)} is missing a \u0275prov definition.`);if(e instanceof Function)return function(e){const t=e.length;if(t>0){const i=sn(t,"?");throw new Error(`Can't resolve all parameters for ${re(e)}: (${i.join(", ")}).`)}const i=function(e){const t=e&&(e[ye]||e[Ce]);if(t){const i=function(e){if(e.hasOwnProperty("name"))return e.name;const t=(""+e).match(/^function\s*([^\s(]+)/);return null===t?"":t[1]}(e);return console.warn(`DEPRECATED: DI is instantiating a token "${i}" that inherits its @Injectable decorator but does not provide one itself.\nThis will become an error in a future version of Angular. Please add @Injectable() to the "${i}" class.`),t}return null}(e);return null!==i?()=>i.factory(e):()=>new e}(e);throw new Error("unreachable")}function to(e,t,i){let n=void 0;if(ro(e)){const t=le(e);return ht(t)||eo(t)}if(no(e))n=()=>le(e.useValue);else if((r=e)&&r.useFactory)n=()=>e.useFactory(...vn(e.deps||[]));else if(function(e){return!(!e||!e.useExisting)}(e))n=()=>mn(le(e.useExisting));else{const t=le(e&&(e.useClass||e.provide));if(!function(e){return!!e.deps}(e))return ht(t)||eo(t);n=()=>new t(...vn(e.deps))}var r;return n}function io(e,t,i=!1){return{factory:e,value:t,multi:i?[]:void 0}}function no(e){return null!==e&&"object"==typeof e&&dn in e}function ro(e){return"function"==typeof e}const so=function(e,t,i){return function(e,t=null,i=null,n){const r=Qs(e,t,i,n);return r._resolveInjectorDefTypes(),r}({name:i},t,e,i)};let oo=(()=>{class e{static create(e,t){return Array.isArray(e)?so(e,t,""):so(e.providers,e.parent,e.name||"")}}return e.THROW_IF_NOT_FOUND=cn,e.NULL=new Ws,e.\u0275prov=pe({token:e,providedIn:"any",factory:()=>mn(zs)}),e.__NG_ELEMENT_ID__=-1,e})();function ao(e,t){di(Tt(e)[1],jt())}function lo(e){let t=Object.getPrototypeOf(e.type.prototype).constructor,i=!0;const n=[e];for(;t;){let r=void 0;if(ct(e))r=t.\u0275cmp||t.\u0275dir;else{if(t.\u0275cmp)throw new Error("Directives cannot inherit Components");r=t.\u0275dir}if(r){if(i){n.push(r);const t=e;t.inputs=co(e.inputs),t.declaredInputs=co(e.declaredInputs),t.outputs=co(e.outputs);const i=r.hostBindings;i&&fo(e,i);const s=r.viewQuery,o=r.contentQueries;if(s&&ho(e,s),o&&uo(e,o),ne(e.inputs,r.inputs),ne(e.declaredInputs,r.declaredInputs),ne(e.outputs,r.outputs),ct(r)&&r.data.animation){const t=e.data;t.animation=(t.animation||[]).concat(r.data.animation)}}const t=r.features;if(t)for(let n=0;n=0;n--){const r=e[n];r.hostVars=t+=r.hostVars,r.hostAttrs=Si(r.hostAttrs,i=Si(i,r.hostAttrs))}}(n)}function co(e){return e===Me?{}:e===He?[]:e}function ho(e,t){const i=e.viewQuery;e.viewQuery=i?(e,n)=>{t(e,n),i(e,n)}:t}function uo(e,t){const i=e.contentQueries;e.contentQueries=i?(e,n,r)=>{t(e,n,r),i(e,n,r)}:t}function fo(e,t){const i=e.hostBindings;e.hostBindings=i?(e,n)=>{t(e,n),i(e,n)}:t}let po=null;function _o(){if(!po){const e=Ie.Symbol;if(e&&e.iterator)po=e.iterator;else{const e=Object.getOwnPropertyNames(Map.prototype);for(let t=0;ta(wt(e[n.index])).target:n.index;if(bt(i)){let o=null;if(!a&&l&&(o=function(e,t,i,n){const r=e.cleanup;if(null!=r)for(let s=0;si?e[i]:null}"string"==typeof e&&(s+=2)}return null}(e,t,r,n.index)),null!==o)(o.__ngLastListenerFn__||o).__ngNextListenerFn__=s,o.__ngLastListenerFn__=s,u=!1;else{s=Vo(n,t,s,!1);const e=i.listen(f.name||p,r,s);h.push(s,e),c&&c.push(r,m,_,_+1)}}else s=Vo(n,t,s,!0),p.addEventListener(r,s,o),h.push(s),c&&c.push(r,m,_,o)}else s=Vo(n,t,s,!1);const d=n.outputs;let f;if(u&&null!==d&&(f=d[r])){const e=f.length;if(e)for(let i=0;i0;)t=t[15],e--;return t}(e,It.lFrame.contextLView))[8]}(e)}function qo(e,t){let i=null;const n=function(e){const t=e.attrs;if(null!=t){const e=t.indexOf(5);if(0==(1&e))return t[e+1]}return null}(e);for(let r=0;r=0}const Go={textEnd:0,key:0,keyEnd:0,value:0,valueEnd:0};function Zo(e){return e.substring(Go.key,Go.keyEnd)}function Yo(e,t){const i=Go.textEnd;return i===t?-1:(t=Go.keyEnd=function(e,t,i){for(;t32;)t++;return t}(e,Go.key=t,i),Xo(e,t,i))}function Xo(e,t,i){for(;t=0;i=Yo(t,i))on(e,Zo(t),!0)}function ia(e,t,i,n){const r=Ft(),s=Ht(),o=Gt(2);s.firstUpdatePass&&sa(s,e,o,n),t!==Gr&&bo(r,o,t)&&la(s,s.data[li()],r,r[11],e,r[o+1]=function(e,t){return null==e||("string"==typeof t?e+=t:"object"==typeof e&&(e=re(Tn(e)))),e}(t,i),n,o)}function na(e,t,i,n){const r=Ht(),s=Gt(2);r.firstUpdatePass&&sa(r,null,s,n);const o=Ft();if(i!==Gr&&bo(o,s,i)){const a=r.data[li()];if(ua(a,n)&&!ra(r,s)){let e=n?a.classesWithoutHost:a.stylesWithoutHost;null!==e&&(i=se(e,i||"")),Ao(r,a,o,i,n)}else!function(e,t,i,n,r,s,o,a){r===Gr&&(r=Fe);let l=0,c=0,h=0=e.expandoStartIndex}function sa(e,t,i,n){const r=e.data;if(null===r[i+1]){const s=r[li()],o=ra(e,i);ua(s,n)&&null===t&&!o&&(t=!1),t=function(e,t,i,n){const r=Xt(e);let s=n?t.residualClasses:t.residualStyles;if(null===r)0===(n?t.classBindings:t.styleBindings)&&(i=aa(i=oa(null,e,t,i,n),t.attrs,n),s=null);else{const o=t.directiveStylingLast;if(-1===o||e[o]!==r)if(i=oa(r,e,t,i,n),null===s){let i=function(e,t,i){const n=i?t.classBindings:t.styleBindings;if(0!==es(n))return e[Qr(n)]}(e,t,n);void 0!==i&&Array.isArray(i)&&(i=oa(null,e,t,i[1],n),i=aa(i,t.attrs,n),function(e,t,i,n){e[Qr(i?t.classBindings:t.styleBindings)]=n}(e,t,n,i))}else s=function(e,t,i){let n=void 0;const r=t.directiveEnd;for(let s=1+t.directiveStylingLast;s0)&&(h=!0)}else c=i;if(r)if(0!==l){const t=Qr(e[a+1]);e[n+1]=Xr(t,a),0!==t&&(e[t+1]=ts(e[t+1],n)),e[a+1]=131071&e[a+1]|n<<17}else e[n+1]=Xr(a,0),0!==a&&(e[a+1]=ts(e[a+1],n)),a=n;else e[n+1]=Xr(l,0),0===a?a=n:e[l+1]=ts(e[l+1],n),l=n;h&&(e[n+1]=Jr(e[n+1])),$o(e,c,n,!0),$o(e,c,n,!1),function(e,t,i,n,r){const s=r?e.residualClasses:e.residualStyles;null!=s&&"string"==typeof t&&ln(s,t)>=0&&(i[n+1]=is(i[n+1]))}(t,c,e,n,s),o=Xr(a,l),s?t.classBindings=o:t.styleBindings=o}(r,s,t,i,o,n)}}function oa(e,t,i,n,r){let s=null;const o=i.directiveEnd;let a=i.directiveStylingLast;for(-1===a?a=i.directiveStart:a++;a0;){const t=e[r],s=Array.isArray(t),l=s?t[1]:t,c=null===l;let h=i[r+1];h===Gr&&(h=c?Fe:void 0);let u=c?an(h,n):l===n?h:void 0;if(s&&!ha(u)&&(u=an(t,n)),ha(u)&&(a=u,o))return a;const d=e[r+1];r=o?Qr(d):es(d)}if(null!==t){let e=s?t.residualClasses:t.residualStyles;null!=e&&(a=an(e,n))}return a}function ha(e){return void 0!==e}function ua(e,t){return 0!=(e.flags&(t?16:32))}function da(e,t=""){const i=Ft(),n=Ht(),r=e+it,s=n.firstCreatePass?ss(n,r,1,t,null):n.data[r],o=i[r]=function(e,t){return bt(e)?e.createText(t):e.createTextNode(t)}(i[11],t);Or(n,i,o,s),Vt(s,!1)}function fa(e){return pa("",e,""),fa}function pa(e,t,i){const n=Ft(),r=wo(n,e,t,i);return r!==Gr&&function(e,t,i){const n=St(t,e);!function(e,t,i){bt(e)?e.setValue(t,i):t.textContent=i}(e[11],n,i)}(n,li(),r),pa}function _a(e,t,i){const n=Ft();return bo(n,Kt(),t)&&vs(Ht(),hi(),n,e,t,n[11],i,!0),_a}function ma(e,t,i){const n=Ft();if(bo(n,Kt(),t)){const r=Ht(),s=hi();vs(r,s,n,e,t,Ns(Xt(r.data),s,n),i,!0)}return ma}const ga=void 0;var va=["en",[["a","p"],["AM","PM"],ga],[["AM","PM"],ga,ga],[["S","M","T","W","T","F","S"],["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],["Su","Mo","Tu","We","Th","Fr","Sa"]],ga,[["J","F","M","A","M","J","J","A","S","O","N","D"],["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],["January","February","March","April","May","June","July","August","September","October","November","December"]],ga,[["B","A"],["BC","AD"],["Before Christ","Anno Domini"]],0,[6,0],["M/d/yy","MMM d, y","MMMM d, y","EEEE, MMMM d, y"],["h:mm a","h:mm:ss a","h:mm:ss a z","h:mm:ss a zzzz"],["{1}, {0}",ga,"{1} 'at' {0}",ga],[".",",",";","%","+","-","E","\xd7","\u2030","\u221e","NaN",":"],["#,##0.###","#,##0%","\xa4#,##0.00","#E0"],"USD","$","US Dollar",{},"ltr",function(e){let t=Math.floor(Math.abs(e)),i=e.toString().replace(/^[^.]*\.?/,"").length;return 1===t&&0===i?1:5}];let ya={};function ba(e){return e in ya||(ya[e]=Ie.ng&&Ie.ng.common&&Ie.ng.common.locales&&Ie.ng.common.locales[e]),ya[e]}var Ca=function(e){return e[e.LocaleId=0]="LocaleId",e[e.DayPeriodsFormat=1]="DayPeriodsFormat",e[e.DayPeriodsStandalone=2]="DayPeriodsStandalone",e[e.DaysFormat=3]="DaysFormat",e[e.DaysStandalone=4]="DaysStandalone",e[e.MonthsFormat=5]="MonthsFormat",e[e.MonthsStandalone=6]="MonthsStandalone",e[e.Eras=7]="Eras",e[e.FirstDayOfWeek=8]="FirstDayOfWeek",e[e.WeekendRange=9]="WeekendRange",e[e.DateFormat=10]="DateFormat",e[e.TimeFormat=11]="TimeFormat",e[e.DateTimeFormat=12]="DateTimeFormat",e[e.NumberSymbols=13]="NumberSymbols",e[e.NumberFormats=14]="NumberFormats",e[e.CurrencyCode=15]="CurrencyCode",e[e.CurrencySymbol=16]="CurrencySymbol",e[e.CurrencyName=17]="CurrencyName",e[e.Currencies=18]="Currencies",e[e.Directionality=19]="Directionality",e[e.PluralCase=20]="PluralCase",e[e.ExtraData=21]="ExtraData",e}({});const wa="en-US";let Sa=wa;function ka(e){var t,i;i="Expected localeId to be defined",null==(t=e)&&function(e,t,i,n){throw new Error("ASSERTION ERROR: "+e+` [Expected=> null != ${t} <=Actual]`)}(i,t),"string"==typeof e&&(Sa=e.toLowerCase().replace(/_/g,"-"))}function xa(e,t,i,n,r){if(e=le(e),Array.isArray(e))for(let s=0;s>20;if(ro(e)||!e.multi){const n=new yi(l,r,xo),f=Ta(a,t,r?h:h+d,u);-1===f?(Mi(Li(c,o),s,a),Ea(s,e,t.length),t.push(a),c.directiveStart++,c.directiveEnd++,r&&(c.providerIndexes+=1048576),i.push(n),o.push(n)):(i[f]=n,o[f]=n)}else{const f=Ta(a,t,h+d,u),p=Ta(a,t,h,h+d),_=f>=0&&i[f],m=p>=0&&i[p];if(r&&!m||!r&&!_){Mi(Li(c,o),s,a);const h=function(e,t,i,n,r){const s=new yi(e,i,xo);return s.multi=[],s.index=t,s.componentProviders=0,Aa(s,r,n&&!i),s}(r?Ra:Oa,i.length,r,n,l);!r&&m&&(i[p].providerFactory=h),Ea(s,e,t.length,0),t.push(a),c.directiveStart++,c.directiveEnd++,r&&(c.providerIndexes+=1048576),i.push(h),o.push(h)}else Ea(s,e,f>-1?f:p,Aa(i[r?p:f],l,!r&&n));!r&&n&&m&&i[p].componentProviders++}}}function Ea(e,t,i,n){const r=ro(t);if(r||t.useClass){const s=(t.useClass||t).prototype.ngOnDestroy;if(s){const o=e.destroyHooks||(e.destroyHooks=[]);if(!r&&t.multi){const e=o.indexOf(i);-1===e?o.push(i,[n,s]):o[e+1].push(n,s)}else o.push(i,s)}}}function Aa(e,t,i){return i&&e.componentProviders++,e.multi.push(t)-1}function Ta(e,t,i,n){for(let r=i;r{i.providersResolver=(i,n)=>function(e,t,i){const n=Ht();if(n.firstCreatePass){const r=ct(e);xa(i,n.data,n.blueprint,r,!0),xa(t,n.data,n.blueprint,r,!1)}}(i,n?n(e):e,t)}}class Pa{}class Ia{resolveComponentFactory(e){throw function(e){const t=Error(`No component factory found for ${re(e)}. Did you add it to @NgModule.entryComponents?`);return t.ngComponent=e,t}(e)}}let Ma=(()=>{class e{}return e.NULL=new Ia,e})();function Fa(...e){}function Ha(e,t){return new ja(kt(e,t))}const Ba=function(){return Ha(jt(),Ft())};let ja=(()=>{class e{constructor(e){this.nativeElement=e}}return e.__NG_ELEMENT_ID__=Ba,e})();function Na(e){return e instanceof ja?e.nativeElement:e}class Va{}let Ua=(()=>{class e{}return e.__NG_ELEMENT_ID__=()=>qa(),e})();const qa=function(){const e=Ft(),t=At(jt().index,e);return function(e){return e[11]}(rt(t)?t:e)};let za=(()=>{class e{}return e.\u0275prov=pe({token:e,providedIn:"root",factory:()=>null}),e})();class Wa{constructor(e){this.full=e,this.major=e.split(".")[0],this.minor=e.split(".")[1],this.patch=e.split(".").slice(2).join(".")}}const $a=new Wa("11.2.7");class Ka{constructor(){}supports(e){return go(e)}create(e){return new Za(e)}}const Ga=(e,t)=>t;class Za{constructor(e){this.length=0,this._linkedRecords=null,this._unlinkedRecords=null,this._previousItHead=null,this._itHead=null,this._itTail=null,this._additionsHead=null,this._additionsTail=null,this._movesHead=null,this._movesTail=null,this._removalsHead=null,this._removalsTail=null,this._identityChangesHead=null,this._identityChangesTail=null,this._trackByFn=e||Ga}forEachItem(e){let t;for(t=this._itHead;null!==t;t=t._next)e(t)}forEachOperation(e){let t=this._itHead,i=this._removalsHead,n=0,r=null;for(;t||i;){const s=!i||t&&t.currentIndex{n=this._trackByFn(t,e),null!==r&&Object.is(r.trackById,n)?(s&&(r=this._verifyReinsertion(r,e,n,t)),Object.is(r.item,e)||this._addIdentityChange(r,e)):(r=this._mismatch(r,e,n,t),s=!0),r=r._next,t++}),this.length=t;return this._truncate(r),this.collection=e,this.isDirty}get isDirty(){return null!==this._additionsHead||null!==this._movesHead||null!==this._removalsHead||null!==this._identityChangesHead}_reset(){if(this.isDirty){let e;for(e=this._previousItHead=this._itHead;null!==e;e=e._next)e._nextPrevious=e._next;for(e=this._additionsHead;null!==e;e=e._nextAdded)e.previousIndex=e.currentIndex;for(this._additionsHead=this._additionsTail=null,e=this._movesHead;null!==e;e=e._nextMoved)e.previousIndex=e.currentIndex;this._movesHead=this._movesTail=null,this._removalsHead=this._removalsTail=null,this._identityChangesHead=this._identityChangesTail=null}}_mismatch(e,t,i,n){let r;return null===e?r=this._itTail:(r=e._prev,this._remove(e)),null!==(e=null===this._unlinkedRecords?null:this._unlinkedRecords.get(i,null))?(Object.is(e.item,t)||this._addIdentityChange(e,t),this._reinsertAfter(e,r,n)):null!==(e=null===this._linkedRecords?null:this._linkedRecords.get(i,n))?(Object.is(e.item,t)||this._addIdentityChange(e,t),this._moveAfter(e,r,n)):e=this._addAfter(new Ya(t,i),r,n),e}_verifyReinsertion(e,t,i,n){let r=null===this._unlinkedRecords?null:this._unlinkedRecords.get(i,null);return null!==r?e=this._reinsertAfter(r,e._prev,n):e.currentIndex!=n&&(e.currentIndex=n,this._addToMoves(e,n)),e}_truncate(e){for(;null!==e;){const t=e._next;this._addToRemovals(this._unlink(e)),e=t}null!==this._unlinkedRecords&&this._unlinkedRecords.clear(),null!==this._additionsTail&&(this._additionsTail._nextAdded=null),null!==this._movesTail&&(this._movesTail._nextMoved=null),null!==this._itTail&&(this._itTail._next=null),null!==this._removalsTail&&(this._removalsTail._nextRemoved=null),null!==this._identityChangesTail&&(this._identityChangesTail._nextIdentityChange=null)}_reinsertAfter(e,t,i){null!==this._unlinkedRecords&&this._unlinkedRecords.remove(e);const n=e._prevRemoved,r=e._nextRemoved;return null===n?this._removalsHead=r:n._nextRemoved=r,null===r?this._removalsTail=n:r._prevRemoved=n,this._insertAfter(e,t,i),this._addToMoves(e,i),e}_moveAfter(e,t,i){return this._unlink(e),this._insertAfter(e,t,i),this._addToMoves(e,i),e}_addAfter(e,t,i){return this._insertAfter(e,t,i),this._additionsTail=null===this._additionsTail?this._additionsHead=e:this._additionsTail._nextAdded=e,e}_insertAfter(e,t,i){const n=null===t?this._itHead:t._next;return e._next=n,e._prev=t,null===n?this._itTail=e:n._prev=e,null===t?this._itHead=e:t._next=e,null===this._linkedRecords&&(this._linkedRecords=new Qa),this._linkedRecords.put(e),e.currentIndex=i,e}_remove(e){return this._addToRemovals(this._unlink(e))}_unlink(e){null!==this._linkedRecords&&this._linkedRecords.remove(e);const t=e._prev,i=e._next;return null===t?this._itHead=i:t._next=i,null===i?this._itTail=t:i._prev=t,e}_addToMoves(e,t){return e.previousIndex===t||(this._movesTail=null===this._movesTail?this._movesHead=e:this._movesTail._nextMoved=e),e}_addToRemovals(e){return null===this._unlinkedRecords&&(this._unlinkedRecords=new Qa),this._unlinkedRecords.put(e),e.currentIndex=null,e._nextRemoved=null,null===this._removalsTail?(this._removalsTail=this._removalsHead=e,e._prevRemoved=null):(e._prevRemoved=this._removalsTail,this._removalsTail=this._removalsTail._nextRemoved=e),e}_addIdentityChange(e,t){return e.item=t,this._identityChangesTail=null===this._identityChangesTail?this._identityChangesHead=e:this._identityChangesTail._nextIdentityChange=e,e}}class Ya{constructor(e,t){this.item=e,this.trackById=t,this.currentIndex=null,this.previousIndex=null,this._nextPrevious=null,this._prev=null,this._next=null,this._prevDup=null,this._nextDup=null,this._prevRemoved=null,this._nextRemoved=null,this._nextAdded=null,this._nextMoved=null,this._nextIdentityChange=null}}class Xa{constructor(){this._head=null,this._tail=null}add(e){null===this._head?(this._head=this._tail=e,e._nextDup=null,e._prevDup=null):(this._tail._nextDup=e,e._prevDup=this._tail,e._nextDup=null,this._tail=e)}get(e,t){let i;for(i=this._head;null!==i;i=i._nextDup)if((null===t||t<=i.currentIndex)&&Object.is(i.trackById,e))return i;return null}remove(e){const t=e._prevDup,i=e._nextDup;return null===t?this._head=i:t._nextDup=i,null===i?this._tail=t:i._prevDup=t,null===this._head}}class Qa{constructor(){this.map=new Map}put(e){const t=e.trackById;let i=this.map.get(t);i||(i=new Xa,this.map.set(t,i)),i.add(e)}get(e,t){const i=this.map.get(e);return i?i.get(e,t):null}remove(e){const t=e.trackById;return this.map.get(t).remove(e)&&this.map.delete(t),e}get isEmpty(){return 0===this.map.size}clear(){this.map.clear()}}function Ja(e,t,i){const n=e.previousIndex;if(null===n)return n;let r=0;return i&&n{if(t&&t.key===i)this._maybeAddToChanges(t,e),this._appendAfter=t,t=t._next;else{const n=this._getOrCreateRecordForKey(i,e);t=this._insertBeforeOrAppend(t,n)}}),t){t._prev&&(t._prev._next=null),this._removalsHead=t;for(let e=t;null!==e;e=e._nextRemoved)e===this._mapHead&&(this._mapHead=null),this._records.delete(e.key),e._nextRemoved=e._next,e.previousValue=e.currentValue,e.currentValue=null,e._prev=null,e._next=null}return this._changesTail&&(this._changesTail._nextChanged=null),this._additionsTail&&(this._additionsTail._nextAdded=null),this.isDirty}_insertBeforeOrAppend(e,t){if(e){const i=e._prev;return t._next=e,t._prev=i,e._prev=t,i&&(i._next=t),e===this._mapHead&&(this._mapHead=t),this._appendAfter=e,e}return this._appendAfter?(this._appendAfter._next=t,t._prev=this._appendAfter):this._mapHead=t,this._appendAfter=t,null}_getOrCreateRecordForKey(e,t){if(this._records.has(e)){const i=this._records.get(e);this._maybeAddToChanges(i,t);const n=i._prev,r=i._next;return n&&(n._next=r),r&&(r._prev=n),i._next=null,i._prev=null,i}const i=new il(e);return this._records.set(e,i),i.currentValue=t,this._addToAdditions(i),i}_reset(){if(this.isDirty){let e;for(this._previousMapHead=this._mapHead,e=this._previousMapHead;null!==e;e=e._next)e._nextPrevious=e._next;for(e=this._changesHead;null!==e;e=e._nextChanged)e.previousValue=e.currentValue;for(e=this._additionsHead;null!=e;e=e._nextAdded)e.previousValue=e.currentValue;this._changesHead=this._changesTail=null,this._additionsHead=this._additionsTail=null,this._removalsHead=null}}_maybeAddToChanges(e,t){Object.is(t,e.currentValue)||(e.previousValue=e.currentValue,e.currentValue=t,this._addToChanges(e))}_addToAdditions(e){null===this._additionsHead?this._additionsHead=this._additionsTail=e:(this._additionsTail._nextAdded=e,this._additionsTail=e)}_addToChanges(e){null===this._changesHead?this._changesHead=this._changesTail=e:(this._changesTail._nextChanged=e,this._changesTail=e)}_forEach(e,t){e instanceof Map?e.forEach(t):Object.keys(e).forEach(i=>t(e[i],i))}}class il{constructor(e){this.key=e,this.previousValue=null,this.currentValue=null,this._nextPrevious=null,this._next=null,this._prev=null,this._nextAdded=null,this._nextRemoved=null,this._nextChanged=null}}function nl(){return new rl([new Ka])}let rl=(()=>{class e{constructor(e){this.factories=e}static create(t,i){if(null!=i){const e=i.factories.slice();t=t.concat(e)}return new e(t)}static extend(t){return{provide:e,useFactory:i=>e.create(t,i||nl()),deps:[[e,new wn,new Cn]]}}find(e){const t=this.factories.find(t=>t.supports(e));if(null!=t)return t;throw new Error(`Cannot find a differ supporting object '${e}' of type '${i=e,i.name||typeof i}'`);var i}}return e.\u0275prov=pe({token:e,providedIn:"root",factory:nl}),e})();function sl(){return new ol([new el])}let ol=(()=>{class e{constructor(e){this.factories=e}static create(t,i){if(i){const e=i.factories.slice();t=t.concat(e)}return new e(t)}static extend(t){return{provide:e,useFactory:i=>e.create(t,i||sl()),deps:[[e,new wn,new Cn]]}}find(e){const t=this.factories.find(t=>t.supports(e));if(t)return t;throw new Error(`Cannot find a differ supporting object '${e}'`)}}return e.\u0275prov=pe({token:e,providedIn:"root",factory:sl}),e})();class al{constructor(e,t){this._lView=e,this._cdRefInjectingView=t,this._appRef=null,this._attachedToViewContainer=!1}get rootNodes(){const e=this._lView,t=e[1];return function e(t,i,n,r,s=!1){for(;null!==n;){const o=i[n.index];if(null!==o&&r.push(wt(o)),st(o))for(let t=nt;t-1&&(yr(e,i),rn(t,i))}this._attachedToViewContainer=!1}br(this._lView[1],this._lView)}onDestroy(e){ms(this._lView[1],this._lView,null,e)}markForCheck(){Ps(this._cdRefInjectingView||this._lView)}detach(){this._lView[2]&=-129}reattach(){this._lView[2]|=128}detectChanges(){Is(this._lView[1],this._lView,this.context)}checkNoChanges(){!function(e,t,i){Wt(!0);try{Is(e,t,i)}finally{Wt(!1)}}(this._lView[1],this._lView,this.context)}attachToViewContainerRef(){if(this._appRef)throw new Error("This view is already attached directly to the ApplicationRef!");this._attachedToViewContainer=!0}detachFromAppRef(){var e;this._appRef=null,Dr(this._lView[1],e=this._lView,e[11],2,null,null)}attachToAppRef(e){if(this._attachedToViewContainer)throw new Error("This view is already attached to a ViewContainer!");this._appRef=e}}class ll extends al{constructor(e){super(e),this._view=e}detectChanges(){Ms(this._view)}checkNoChanges(){!function(e){Wt(!0);try{Ms(e)}finally{Wt(!1)}}(this._view)}get context(){return null}}const cl=ul;let hl=(()=>{class e{}return e.__NG_ELEMENT_ID__=cl,e.__ChangeDetectorRef__=!0,e})();function ul(e=!1){return function(e,t,i){if(!i&&at(e)){const i=At(e.index,t);return new al(i,i)}return 47&e.type?new al(t[16],t):null}(jt(),Ft(),e)}const dl=[new el],fl=new rl([new Ka]),pl=new ol(dl),_l=function(){return yl(jt(),Ft())};let ml=(()=>{class e{}return e.__NG_ELEMENT_ID__=_l,e})();const gl=ml,vl=class extends gl{constructor(e,t,i){super(),this._declarationLView=e,this._declarationTContainer=t,this.elementRef=i}createEmbeddedView(e){const t=this._declarationTContainer.tViews,i=rs(this._declarationLView,t,e,16,null,t.declTNode,null,null,null,null);i[17]=this._declarationLView[this._declarationTContainer.index];const n=this._declarationLView[19];return null!==n&&(i[19]=n.createEmbeddedView(t)),as(t,i,e),new al(i)}};function yl(e,t){return 4&e.type?new vl(t,e,Ha(e,t)):null}class bl{}class Cl{}const wl=function(){return Tl(jt(),Ft())};let Sl=(()=>{class e{}return e.__NG_ELEMENT_ID__=wl,e})();const kl=Sl,xl=class extends kl{constructor(e,t,i){super(),this._lContainer=e,this._hostTNode=t,this._hostLView=i}get element(){return Ha(this._hostTNode,this._hostLView)}get injector(){return new $i(this._hostTNode,this._hostLView)}get parentInjector(){const e=Ii(this._hostTNode,this._hostLView);if(xi(e)){const t=Ai(e,this._hostLView),i=Ei(e);return new $i(t[1].data[i+8],t)}return new $i(null,this._hostLView)}clear(){for(;this.length>0;)this.remove(this.length-1)}get(e){const t=El(this._lContainer);return null!==t&&t[e]||null}get length(){return this._lContainer.length-nt}createEmbeddedView(e,t,i){const n=e.createEmbeddedView(t||{});return this.insert(n,i),n}createComponent(e,t,i,n,r){const s=i||this.parentInjector;if(!r&&null==e.ngModule&&s){const e=s.get(bl,null);e&&(r=e)}const o=e.create(s,n,void 0,r);return this.insert(o.hostView,t),o}insert(e,t){const i=e._lView,n=i[1];if(st(i[3])){const t=this.indexOf(e);if(-1!==t)this.detach(t);else{const t=i[3],n=new xl(t,t[6],t[3]);n.detach(n.indexOf(e))}}const r=this._adjustIndex(t),s=this._lContainer;!function(e,t,i,n){const r=nt+n,s=i.length;n>0&&(i[r-1][4]=t),nar});class Pl extends Pa{constructor(e,t){super(),this.componentDef=e,this.ngModule=t,this.componentType=e.type,this.selector=e.selectors.map(Kr).join(","),this.ngContentSelectors=e.ngContentSelectors?e.ngContentSelectors:[],this.isBoundToModule=!!t}get inputs(){return Ll(this.componentDef.inputs)}get outputs(){return Ll(this.componentDef.outputs)}create(e,t,i,n){const r=(n=n||this.ngModule)?function(e,t){return{get:(i,n,r)=>{const s=e.get(i,Ol,r);return s!==Ol||n===Ol?s:t.get(i,n,r)}}}(e,n.injector):e,s=r.get(Va,Ct),o=r.get(za,null),a=s.createRenderer(null,this.componentDef),l=this.componentDef.selectors[0][0]||"div",c=i?function(e,t,i){if(bt(e))return e.selectRootElement(t,i===Oe.ShadowDom);let n="string"==typeof t?e.querySelector(t):t;return n.textContent="",n}(a,i,this.componentDef.encapsulation):gr(s.createRenderer(null,this.componentDef),l,function(e){const t=e.toLowerCase();return"svg"===t?gt:"math"===t?"http://www.w3.org/1998/MathML/":null}(l)),h=this.componentDef.onPush?576:528,u={components:[],scheduler:ar,clean:Hs,playerHandler:null,flags:0},d=_s(0,null,null,1,0,null,null,null,null,null),f=rs(null,d,u,h,null,null,s,a,o,r);let p,_;ii(f);try{const e=function(e,t,i,n,r,s){const o=i[1];i[20]=e;const a=ss(o,20,2,"#host",null),l=a.mergedAttrs=t.hostAttrs;null!==l&&(qs(a,l,!0),null!==e&&(bi(r,e,l),null!==a.classes&&Mr(r,e,a.classes),null!==a.styles&&Ir(r,e,a.styles)));const c=n.createRenderer(e,t),h=rs(i,ps(t),null,t.onPush?64:16,i[20],a,n,c,null,null);return o.firstCreatePass&&(Mi(Li(a,i),o,t.type),ws(o,a),ks(a,i.length,1)),Ds(i,h),i[20]=h}(c,this.componentDef,f,s,a);if(c)if(i)bi(a,c,["ng-version",$a.full]);else{const{attrs:e,classes:t}=function(e){const t=[],i=[];let n=1,r=2;for(;n0&&Mr(a,c,t.join(" "))}if(_=xt(d,it),void 0!==t){const e=_.projection=[];for(let i=0;ie(o,t)),t.contentQueries){const e=jt();t.contentQueries(1,o,e.directiveStart)}const a=jt();return!s.firstCreatePass||null===t.hostBindings&&null===t.hostAttrs||(ci(a.index),bs(i[1],a,0,a.directiveStart,a.directiveEnd,t),Cs(t,o)),o}(e,this.componentDef,f,u,[ao]),as(d,f,null)}finally{ai()}return new Il(this.componentType,p,Ha(_,f),f,_)}}class Il extends class{}{constructor(e,t,i,n,r){super(),this.location=i,this._rootLView=n,this._tNode=r,this.instance=t,this.hostView=this.changeDetectorRef=new ll(n),this.componentType=e}get injector(){return new $i(this._tNode,this._rootLView)}destroy(){this.hostView.destroy()}onDestroy(e){this.hostView.onDestroy(e)}}const Ml=new Map;class Fl extends bl{constructor(e,t){super(),this._parent=t,this._bootstrapComponents=[],this.injector=this,this.destroyCbs=[],this.componentFactoryResolver=new Rl(this);const i=tt(e),n=e[Ue]||null;n&&ka(n),this._bootstrapComponents=cr(i.bootstrap),this._r3Injector=Qs(e,t,[{provide:bl,useValue:this},{provide:Ma,useValue:this.componentFactoryResolver}],re(e)),this._r3Injector._resolveInjectorDefTypes(),this.instance=this.get(e)}get(e,t=oo.THROW_IF_NOT_FOUND,i=Se.Default){return e===oo||e===bl||e===zs?this:this._r3Injector.get(e,t,i)}destroy(){const e=this._r3Injector;!e.destroyed&&e.destroy(),this.destroyCbs.forEach(e=>e()),this.destroyCbs=null}onDestroy(e){this.destroyCbs.push(e)}}class Hl extends Cl{constructor(e){super(),this.moduleType=e,null!==tt(e)&&function(e){const t=new Set;!function e(i){const n=tt(i,!0),r=n.id;null!==r&&(function(e,t,i){if(t&&t!==i)throw new Error(`Duplicate module registered for ${e} - ${re(t)} vs ${re(t.name)}`)}(r,Ml.get(r),i),Ml.set(r,i));const s=cr(n.imports);for(const o of s)t.has(o)||(t.add(o),e(o))}(e)}(e)}create(e){return new Fl(this.moduleType,e)}}function Bl(e,t,i){const n=$t()+e,r=Ft();return r[n]===Gr?yo(r,n,i?t.call(i):t()):function(e,t){return e[t]}(r,n)}function jl(e,t){const i=e[t];return i===Gr?void 0:i}function Nl(e,t){const i=Ht();let n;const r=e+it;i.firstCreatePass?(n=function(e,t){if(t)for(let i=t.length-1;i>=0;i--){const n=t[i];if(e===n.name)return n}throw new he("302",`The pipe '${e}' could not be found!`)}(t,i.pipeRegistry),i.data[r]=n,n.onDestroy&&(i.destroyHooks||(i.destroyHooks=[])).push(r,n.onDestroy)):n=i.data[r];const s=n.factory||(n.factory=ht(n.type)),o=xe(xo);try{const e=Oi(!1),t=s();return Oi(e),function(e,t,i,n){i>=e.data.length&&(e.data[i]=null,e.blueprint[i]=null),t[i]=n}(i,Ft(),r,t),t}finally{xe(o)}}function Vl(e,t,i){const n=e+it,r=Ft(),s=Et(r,n);return function(e,t){return mo.isWrapped(t)&&(t=mo.unwrap(t),e[It.lFrame.bindingIndex]=Gr),t}(r,function(e,t){return e[1].data[t].pure}(r,n)?function(e,t,i,n,r,s){const o=t+i;return bo(e,o,r)?yo(e,o+1,s?n.call(s,r):n(r)):jl(e,o+1)}(r,$t(),t,s.transform,i,s):s.transform(i))}const Ul=class extends S{constructor(e=!1){super(),this.__isAsync=e}emit(e){super.next(e)}subscribe(e,t,i){let n,r=e=>null,s=()=>null;e&&"object"==typeof e?(n=this.__isAsync?t=>{setTimeout(()=>e.next(t))}:t=>{e.next(t)},e.error&&(r=this.__isAsync?t=>{setTimeout(()=>e.error(t))}:t=>{e.error(t)}),e.complete&&(s=this.__isAsync?()=>{setTimeout(()=>e.complete())}:()=>{e.complete()})):(n=this.__isAsync?t=>{setTimeout(()=>e(t))}:t=>{e(t)},t&&(r=this.__isAsync?e=>{setTimeout(()=>t(e))}:e=>{t(e)}),i&&(s=this.__isAsync?()=>{setTimeout(()=>i())}:()=>{i()}));const o=super.subscribe(n,r,s);return e instanceof u&&e.add(o),o}};function ql(){return this._results[_o()]()}class zl{constructor(e=!1){this._emitDistinctChangesOnly=e,this.dirty=!0,this._results=[],this._changesDetected=!1,this._changes=null,this.length=0,this.first=void 0,this.last=void 0;const t=_o(),i=zl.prototype;i[t]||(i[t]=ql)}get changes(){return this._changes||(this._changes=new Ul)}get(e){return this._results[e]}map(e){return this._results.map(e)}filter(e){return this._results.filter(e)}find(e){return this._results.find(e)}reduce(e,t){return this._results.reduce(e,t)}forEach(e){this._results.forEach(e)}some(e){return this._results.some(e)}toArray(){return this._results.slice()}toString(){return this._results.toString()}reset(e,t){const i=this;i.dirty=!1;const n=function e(t,i){void 0===i&&(i=t);for(let n=0;n0)r.push(a[t/2]);else{const s=o[t+1],a=i[-n];for(let t=nt;t{class e{constructor(e){this.appInits=e,this.resolve=Fa,this.reject=Fa,this.initialized=!1,this.done=!1,this.donePromise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}runInitializers(){if(this.initialized)return;const e=[],t=()=>{this.done=!0,this.resolve()};if(this.appInits)for(let i=0;i{t()}).catch(e=>{this.reject(e)}),0===e.length&&t(),this.initialized=!0}}return e.\u0275fac=function(t){return new(t||e)(mn(ac,8))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const cc=new Qi("AppId"),hc={provide:cc,useFactory:function(){return`${uc()}${uc()}${uc()}`},deps:[]};function uc(){return String.fromCharCode(97+Math.floor(25*Math.random()))}const dc=new Qi("Platform Initializer"),fc=new Qi("Platform ID"),pc=new Qi("appBootstrapListener");let _c=(()=>{class e{log(e){console.log(e)}warn(e){console.warn(e)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const mc=new Qi("LocaleId"),gc=new Qi("DefaultCurrencyCode");class vc{constructor(e,t){this.ngModuleFactory=e,this.componentFactories=t}}const yc=function(e){return new Hl(e)},bc=yc,Cc=function(e){return Promise.resolve(yc(e))},wc=function(e){const t=yc(e),i=cr(tt(e).declarations).reduce((e,t)=>{const i=et(t);return i&&e.push(new Pl(i)),e},[]);return new vc(t,i)},Sc=wc,kc=function(e){return Promise.resolve(wc(e))};let xc=(()=>{class e{constructor(){this.compileModuleSync=bc,this.compileModuleAsync=Cc,this.compileModuleAndAllComponentsSync=Sc,this.compileModuleAndAllComponentsAsync=kc}clearCache(){}clearCacheFor(e){}getModuleId(e){}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const Ec=(()=>Promise.resolve(0))();function Ac(e){"undefined"==typeof Zone?Ec.then(()=>{e&&e.apply(null,null)}):Zone.current.scheduleMicroTask("scheduleMicrotask",e)}class Tc{constructor({enableLongStackTrace:e=!1,shouldCoalesceEventChangeDetection:t=!1,shouldCoalesceRunChangeDetection:i=!1}){if(this.hasPendingMacrotasks=!1,this.hasPendingMicrotasks=!1,this.isStable=!0,this.onUnstable=new Ul(!1),this.onMicrotaskEmpty=new Ul(!1),this.onStable=new Ul(!1),this.onError=new Ul(!1),"undefined"==typeof Zone)throw new Error("In this configuration Angular requires Zone.js");Zone.assertZonePatched();const n=this;n._nesting=0,n._outer=n._inner=Zone.current,Zone.TaskTrackingZoneSpec&&(n._inner=n._inner.fork(new Zone.TaskTrackingZoneSpec)),e&&Zone.longStackTraceZoneSpec&&(n._inner=n._inner.fork(Zone.longStackTraceZoneSpec)),n.shouldCoalesceEventChangeDetection=!i&&t,n.shouldCoalesceRunChangeDetection=i,n.lastRequestAnimationFrameId=-1,n.nativeRequestAnimationFrame=function(){let e=Ie.requestAnimationFrame,t=Ie.cancelAnimationFrame;if("undefined"!=typeof Zone&&e&&t){const i=e[Zone.__symbol__("OriginalDelegate")];i&&(e=i);const n=t[Zone.__symbol__("OriginalDelegate")];n&&(t=n)}return{nativeRequestAnimationFrame:e,nativeCancelAnimationFrame:t}}().nativeRequestAnimationFrame,function(e){const t=()=>{!function(e){-1===e.lastRequestAnimationFrameId&&(e.lastRequestAnimationFrameId=e.nativeRequestAnimationFrame.call(Ie,()=>{e.fakeTopEventTask||(e.fakeTopEventTask=Zone.root.scheduleEventTask("fakeTopEventTask",()=>{e.lastRequestAnimationFrameId=-1,Lc(e),Rc(e)},void 0,()=>{},()=>{})),e.fakeTopEventTask.invoke()}),Lc(e))}(e)};e._inner=e._inner.fork({name:"angular",properties:{isAngularZone:!0},onInvokeTask:(i,n,r,s,o,a)=>{try{return Dc(e),i.invokeTask(r,s,o,a)}finally{(e.shouldCoalesceEventChangeDetection&&"eventTask"===s.type||e.shouldCoalesceRunChangeDetection)&&t(),Pc(e)}},onInvoke:(i,n,r,s,o,a,l)=>{try{return Dc(e),i.invoke(r,s,o,a,l)}finally{e.shouldCoalesceRunChangeDetection&&t(),Pc(e)}},onHasTask:(t,i,n,r)=>{t.hasTask(n,r),i===n&&("microTask"==r.change?(e._hasPendingMicrotasks=r.microTask,Lc(e),Rc(e)):"macroTask"==r.change&&(e.hasPendingMacrotasks=r.macroTask))},onHandleError:(t,i,n,r)=>(t.handleError(n,r),e.runOutsideAngular(()=>e.onError.emit(r)),!1)})}(n)}static isInAngularZone(){return!0===Zone.current.get("isAngularZone")}static assertInAngularZone(){if(!Tc.isInAngularZone())throw new Error("Expected to be in Angular Zone, but it is not!")}static assertNotInAngularZone(){if(Tc.isInAngularZone())throw new Error("Expected to not be in Angular Zone, but it is!")}run(e,t,i){return this._inner.run(e,t,i)}runTask(e,t,i,n){const r=this._inner,s=r.scheduleEventTask("NgZoneEvent: "+n,e,Oc,Fa,Fa);try{return r.runTask(s,t,i)}finally{r.cancelTask(s)}}runGuarded(e,t,i){return this._inner.runGuarded(e,t,i)}runOutsideAngular(e){return this._outer.run(e)}}const Oc={};function Rc(e){if(0==e._nesting&&!e.hasPendingMicrotasks&&!e.isStable)try{e._nesting++,e.onMicrotaskEmpty.emit(null)}finally{if(e._nesting--,!e.hasPendingMicrotasks)try{e.runOutsideAngular(()=>e.onStable.emit(null))}finally{e.isStable=!0}}}function Lc(e){e.hasPendingMicrotasks=!!(e._hasPendingMicrotasks||(e.shouldCoalesceEventChangeDetection||e.shouldCoalesceRunChangeDetection)&&-1!==e.lastRequestAnimationFrameId)}function Dc(e){e._nesting++,e.isStable&&(e.isStable=!1,e.onUnstable.emit(null))}function Pc(e){e._nesting--,Rc(e)}class Ic{constructor(){this.hasPendingMicrotasks=!1,this.hasPendingMacrotasks=!1,this.isStable=!0,this.onUnstable=new Ul,this.onMicrotaskEmpty=new Ul,this.onStable=new Ul,this.onError=new Ul}run(e,t,i){return e.apply(t,i)}runGuarded(e,t,i){return e.apply(t,i)}runOutsideAngular(e){return e()}runTask(e,t,i,n){return e.apply(t,i)}}let Mc=(()=>{class e{constructor(e){this._ngZone=e,this._pendingCount=0,this._isZoneStable=!0,this._didWork=!1,this._callbacks=[],this.taskTrackingZone=null,this._watchAngularEvents(),e.run(()=>{this.taskTrackingZone="undefined"==typeof Zone?null:Zone.current.get("TaskTrackingZone")})}_watchAngularEvents(){this._ngZone.onUnstable.subscribe({next:()=>{this._didWork=!0,this._isZoneStable=!1}}),this._ngZone.runOutsideAngular(()=>{this._ngZone.onStable.subscribe({next:()=>{Tc.assertNotInAngularZone(),Ac(()=>{this._isZoneStable=!0,this._runCallbacksIfReady()})}})})}increasePendingRequestCount(){return this._pendingCount+=1,this._didWork=!0,this._pendingCount}decreasePendingRequestCount(){if(this._pendingCount-=1,this._pendingCount<0)throw new Error("pending async requests below zero");return this._runCallbacksIfReady(),this._pendingCount}isStable(){return this._isZoneStable&&0===this._pendingCount&&!this._ngZone.hasPendingMacrotasks}_runCallbacksIfReady(){if(this.isStable())Ac(()=>{for(;0!==this._callbacks.length;){let e=this._callbacks.pop();clearTimeout(e.timeoutId),e.doneCb(this._didWork)}this._didWork=!1});else{let e=this.getPendingTasks();this._callbacks=this._callbacks.filter(t=>!t.updateCb||!t.updateCb(e)||(clearTimeout(t.timeoutId),!1)),this._didWork=!0}}getPendingTasks(){return this.taskTrackingZone?this.taskTrackingZone.macroTasks.map(e=>({source:e.source,creationLocation:e.creationLocation,data:e.data})):[]}addCallback(e,t,i){let n=-1;t&&t>0&&(n=setTimeout(()=>{this._callbacks=this._callbacks.filter(e=>e.timeoutId!==n),e(this._didWork,this.getPendingTasks())},t)),this._callbacks.push({doneCb:e,timeoutId:n,updateCb:i})}whenStable(e,t,i){if(i&&!this.taskTrackingZone)throw new Error('Task tracking zone is required when passing an update callback to whenStable(). Is "zone.js/dist/task-tracking.js" loaded?');this.addCallback(e,t,i),this._runCallbacksIfReady()}getPendingRequestCount(){return this._pendingCount}findProviders(e,t,i){return[]}}return e.\u0275fac=function(t){return new(t||e)(mn(Tc))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),Fc=(()=>{class e{constructor(){this._applications=new Map,jc.addToWindow(this)}registerApplication(e,t){this._applications.set(e,t)}unregisterApplication(e){this._applications.delete(e)}unregisterAllApplications(){this._applications.clear()}getTestability(e){return this._applications.get(e)||null}getAllTestabilities(){return Array.from(this._applications.values())}getAllRootElements(){return Array.from(this._applications.keys())}findTestabilityInTree(e,t=!0){return jc.findTestabilityInTree(this,e,t)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();class Hc{addToWindow(e){}findTestabilityInTree(e,t,i){return null}}let Bc,jc=new Hc,Nc=!0,Vc=!1;function Uc(){return Vc=!0,Nc}const qc=new Qi("AllowMultipleToken");class zc{constructor(e,t){this.name=e,this.token=t}}function Wc(e,t,i=[]){const n="Platform: "+t,r=new Qi(n);return(t=[])=>{let s=$c();if(!s||s.injector.get(qc,!1))if(e)e(i.concat(t).concat({provide:r,useValue:!0}));else{const e=i.concat(t).concat({provide:r,useValue:!0},{provide:$s,useValue:"platform"});!function(e){if(Bc&&!Bc.destroyed&&!Bc.injector.get(qc,!1))throw new Error("There can be only one platform. Destroy the previous one to create a new one.");Bc=e.get(Kc);const t=e.get(dc,null);t&&t.forEach(e=>e())}(oo.create({providers:e,name:n}))}return function(e){const t=$c();if(!t)throw new Error("No platform exists!");if(!t.injector.get(e,null))throw new Error("A platform with a different configuration has been created. Please destroy it first.");return t}(r)}}function $c(){return Bc&&!Bc.destroyed?Bc:null}let Kc=(()=>{class e{constructor(e){this._injector=e,this._modules=[],this._destroyListeners=[],this._destroyed=!1}bootstrapModuleFactory(e,t){const i=function(e,t){let i;return i="noop"===e?new Ic:("zone.js"===e?void 0:e)||new Tc({enableLongStackTrace:Uc(),shouldCoalesceEventChangeDetection:!!(null==t?void 0:t.ngZoneEventCoalescing),shouldCoalesceRunChangeDetection:!!(null==t?void 0:t.ngZoneRunCoalescing)}),i}(t?t.ngZone:void 0,{ngZoneEventCoalescing:t&&t.ngZoneEventCoalescing||!1,ngZoneRunCoalescing:t&&t.ngZoneRunCoalescing||!1}),n=[{provide:Tc,useValue:i}];return i.run(()=>{const t=oo.create({providers:n,parent:this.injector,name:e.moduleType.name}),r=e.create(t),s=r.injector.get(sr,null);if(!s)throw new Error("No ErrorHandler. Is platform module (BrowserModule) included?");return i.runOutsideAngular(()=>{const e=i.onError.subscribe({next:e=>{s.handleError(e)}});r.onDestroy(()=>{Yc(this._modules,r),e.unsubscribe()})}),function(e,t,i){try{const n=i();return Io(n)?n.catch(i=>{throw t.runOutsideAngular(()=>e.handleError(i)),i}):n}catch(n){throw t.runOutsideAngular(()=>e.handleError(n)),n}}(s,i,()=>{const e=r.injector.get(lc);return e.runInitializers(),e.donePromise.then(()=>(ka(r.injector.get(mc,wa)||wa),this._moduleDoBootstrap(r),r))})})}bootstrapModule(e,t=[]){const i=Gc({},t);return function(e,t,i){const n=new Hl(i);return Promise.resolve(n)}(0,0,e).then(e=>this.bootstrapModuleFactory(e,i))}_moduleDoBootstrap(e){const t=e.injector.get(Zc);if(e._bootstrapComponents.length>0)e._bootstrapComponents.forEach(e=>t.bootstrap(e));else{if(!e.instance.ngDoBootstrap)throw new Error(`The module ${re(e.instance.constructor)} was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`);e.instance.ngDoBootstrap(t)}this._modules.push(e)}onDestroy(e){this._destroyListeners.push(e)}get injector(){return this._injector}destroy(){if(this._destroyed)throw new Error("The platform has already been destroyed!");this._modules.slice().forEach(e=>e.destroy()),this._destroyListeners.forEach(e=>e()),this._destroyed=!0}get destroyed(){return this._destroyed}}return e.\u0275fac=function(t){return new(t||e)(mn(oo))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();function Gc(e,t){return Array.isArray(t)?t.reduce(Gc,e):Object.assign(Object.assign({},e),t)}let Zc=(()=>{class e{constructor(e,t,i,n,r){this._zone=e,this._injector=t,this._exceptionHandler=i,this._componentFactoryResolver=n,this._initStatus=r,this._bootstrapListeners=[],this._views=[],this._runningTick=!1,this._stable=!0,this.componentTypes=[],this.components=[],this._onMicrotaskEmptySubscription=this._zone.onMicrotaskEmpty.subscribe({next:()=>{this._zone.run(()=>{this.tick()})}});const s=new v(e=>{this._stable=this._zone.isStable&&!this._zone.hasPendingMacrotasks&&!this._zone.hasPendingMicrotasks,this._zone.runOutsideAngular(()=>{e.next(this._stable),e.complete()})}),o=new v(e=>{let t;this._zone.runOutsideAngular(()=>{t=this._zone.onStable.subscribe(()=>{Tc.assertNotInAngularZone(),Ac(()=>{this._stable||this._zone.hasPendingMacrotasks||this._zone.hasPendingMicrotasks||(this._stable=!0,e.next(!0))})})});const i=this._zone.onUnstable.subscribe(()=>{Tc.assertInAngularZone(),this._stable&&(this._stable=!1,this._zone.runOutsideAngular(()=>{e.next(!1)}))});return()=>{t.unsubscribe(),i.unsubscribe()}});this.isStable=W(s,o.pipe(te()))}bootstrap(e,t){if(!this._initStatus.done)throw new Error("Cannot bootstrap as there are still asynchronous initializers running. Bootstrap components in the `ngDoBootstrap` method of the root module.");let i;i=e instanceof Pa?e:this._componentFactoryResolver.resolveComponentFactory(e),this.componentTypes.push(i.componentType);const n=i.isBoundToModule?void 0:this._injector.get(bl),r=i.create(oo.NULL,[],t||i.selector,n),s=r.location.nativeElement,o=r.injector.get(Mc,null),a=o&&r.injector.get(Fc);return o&&a&&a.registerApplication(s,o),r.onDestroy(()=>{this.detachView(r.hostView),Yc(this.components,r),a&&a.unregisterApplication(s)}),this._loadComponent(r),r}tick(){if(this._runningTick)throw new Error("ApplicationRef.tick is called recursively");try{this._runningTick=!0;for(let e of this._views)e.detectChanges()}catch(e){this._zone.runOutsideAngular(()=>this._exceptionHandler.handleError(e))}finally{this._runningTick=!1}}attachView(e){const t=e;this._views.push(t),t.attachToAppRef(this)}detachView(e){const t=e;Yc(this._views,t),t.detachFromAppRef()}_loadComponent(e){this.attachView(e.hostView),this.tick(),this.components.push(e),this._injector.get(pc,[]).concat(this._bootstrapListeners).forEach(t=>t(e))}ngOnDestroy(){this._views.slice().forEach(e=>e.destroy()),this._onMicrotaskEmptySubscription.unsubscribe()}get viewCount(){return this._views.length}}return e.\u0275fac=function(t){return new(t||e)(mn(Tc),mn(oo),mn(sr),mn(Ma),mn(lc))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();function Yc(e,t){const i=e.indexOf(t);i>-1&&e.splice(i,1)}class Xc{}class Qc{}const Jc={factoryPathPrefix:"",factoryPathSuffix:".ngfactory"};let eh=(()=>{class e{constructor(e,t){this._compiler=e,this._config=t||Jc}load(e){return this.loadAndCompile(e)}loadAndCompile(e){let[t,n]=e.split("#");return void 0===n&&(n="default"),i("zn8P")(t).then(e=>e[n]).then(e=>th(e,t,n)).then(e=>this._compiler.compileModuleAsync(e))}loadFactory(e){let[t,n]=e.split("#"),r="NgFactory";return void 0===n&&(n="default",r=""),i("zn8P")(this._config.factoryPathPrefix+t+this._config.factoryPathSuffix).then(e=>e[n+r]).then(e=>th(e,t,n))}}return e.\u0275fac=function(t){return new(t||e)(mn(xc),mn(Qc,8))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();function th(e,t,i){if(!e)throw new Error(`Cannot find '${i}' in '${t}'`);return e}const ih=Wc(null,"core",[{provide:fc,useValue:"unknown"},{provide:Kc,deps:[oo]},{provide:Fc,deps:[]},{provide:_c,deps:[]}]),nh=[{provide:Zc,useClass:Zc,deps:[Tc,oo,sr,Ma,lc]},{provide:Dl,deps:[Tc],useFactory:function(e){let t=[];return e.onStable.subscribe(()=>{for(;t.length;)t.pop()()}),function(e){t.push(e)}}},{provide:lc,useClass:lc,deps:[[new Cn,ac]]},{provide:xc,useClass:xc,deps:[]},hc,{provide:rl,useFactory:function(){return fl},deps:[]},{provide:ol,useFactory:function(){return pl},deps:[]},{provide:mc,useFactory:function(e){return ka(e=e||"undefined"!=typeof $localize&&$localize.locale||wa),e},deps:[[new bn(mc),new Cn,new wn]]},{provide:gc,useValue:"USD"}];let rh=(()=>{class e{constructor(e){}}return e.\u0275fac=function(t){return new(t||e)(mn(Zc))},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:nh}),e})(),sh=null;function oh(){return sh}const ah=new Qi("DocumentToken");let lh=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({factory:ch,token:e,providedIn:"platform"}),e})();function ch(){return mn(uh)}const hh=new Qi("Location Initialized");let uh=(()=>{class e extends lh{constructor(e){super(),this._doc=e,this._init()}_init(){this.location=oh().getLocation(),this._history=oh().getHistory()}getBaseHrefFromDOM(){return oh().getBaseHref(this._doc)}onPopState(e){oh().getGlobalEventTarget(this._doc,"window").addEventListener("popstate",e,!1)}onHashChange(e){oh().getGlobalEventTarget(this._doc,"window").addEventListener("hashchange",e,!1)}get href(){return this.location.href}get protocol(){return this.location.protocol}get hostname(){return this.location.hostname}get port(){return this.location.port}get pathname(){return this.location.pathname}get search(){return this.location.search}get hash(){return this.location.hash}set pathname(e){this.location.pathname=e}pushState(e,t,i){dh()?this._history.pushState(e,t,i):this.location.hash=i}replaceState(e,t,i){dh()?this._history.replaceState(e,t,i):this.location.hash=i}forward(){this._history.forward()}back(){this._history.back()}getState(){return this._history.state}}return e.\u0275fac=function(t){return new(t||e)(mn(ah))},e.\u0275prov=pe({factory:fh,token:e,providedIn:"platform"}),e})();function dh(){return!!window.history.pushState}function fh(){return new uh(mn(ah))}function ph(e,t){if(0==e.length)return t;if(0==t.length)return e;let i=0;return e.endsWith("/")&&i++,t.startsWith("/")&&i++,2==i?e+t.substring(1):1==i?e+t:e+"/"+t}function _h(e){const t=e.match(/#|\?|$/),i=t&&t.index||e.length;return e.slice(0,i-("/"===e[i-1]?1:0))+e.slice(i)}function mh(e){return e&&"?"!==e[0]?"?"+e:e}let gh=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({factory:vh,token:e,providedIn:"root"}),e})();function vh(e){const t=mn(ah).location;return new bh(mn(lh),t&&t.origin||"")}const yh=new Qi("appBaseHref");let bh=(()=>{class e extends gh{constructor(e,t){if(super(),this._platformLocation=e,null==t&&(t=this._platformLocation.getBaseHrefFromDOM()),null==t)throw new Error("No base href set. Please provide a value for the APP_BASE_HREF token or add a base element to the document.");this._baseHref=t}onPopState(e){this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e)}getBaseHref(){return this._baseHref}prepareExternalUrl(e){return ph(this._baseHref,e)}path(e=!1){const t=this._platformLocation.pathname+mh(this._platformLocation.search),i=this._platformLocation.hash;return i&&e?`${t}${i}`:t}pushState(e,t,i,n){const r=this.prepareExternalUrl(i+mh(n));this._platformLocation.pushState(e,t,r)}replaceState(e,t,i,n){const r=this.prepareExternalUrl(i+mh(n));this._platformLocation.replaceState(e,t,r)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}}return e.\u0275fac=function(t){return new(t||e)(mn(lh),mn(yh,8))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),Ch=(()=>{class e extends gh{constructor(e,t){super(),this._platformLocation=e,this._baseHref="",null!=t&&(this._baseHref=t)}onPopState(e){this._platformLocation.onPopState(e),this._platformLocation.onHashChange(e)}getBaseHref(){return this._baseHref}path(e=!1){let t=this._platformLocation.hash;return null==t&&(t="#"),t.length>0?t.substring(1):t}prepareExternalUrl(e){const t=ph(this._baseHref,e);return t.length>0?"#"+t:t}pushState(e,t,i,n){let r=this.prepareExternalUrl(i+mh(n));0==r.length&&(r=this._platformLocation.pathname),this._platformLocation.pushState(e,t,r)}replaceState(e,t,i,n){let r=this.prepareExternalUrl(i+mh(n));0==r.length&&(r=this._platformLocation.pathname),this._platformLocation.replaceState(e,t,r)}forward(){this._platformLocation.forward()}back(){this._platformLocation.back()}}return e.\u0275fac=function(t){return new(t||e)(mn(lh),mn(yh,8))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),wh=(()=>{class e{constructor(e,t){this._subject=new Ul,this._urlChangeListeners=[],this._platformStrategy=e;const i=this._platformStrategy.getBaseHref();this._platformLocation=t,this._baseHref=_h(kh(i)),this._platformStrategy.onPopState(e=>{this._subject.emit({url:this.path(!0),pop:!0,state:e.state,type:e.type})})}path(e=!1){return this.normalize(this._platformStrategy.path(e))}getState(){return this._platformLocation.getState()}isCurrentPathEqualTo(e,t=""){return this.path()==this.normalize(e+mh(t))}normalize(t){return e.stripTrailingSlash(function(e,t){return e&&t.startsWith(e)?t.substring(e.length):t}(this._baseHref,kh(t)))}prepareExternalUrl(e){return e&&"/"!==e[0]&&(e="/"+e),this._platformStrategy.prepareExternalUrl(e)}go(e,t="",i=null){this._platformStrategy.pushState(i,"",e,t),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+mh(t)),i)}replaceState(e,t="",i=null){this._platformStrategy.replaceState(i,"",e,t),this._notifyUrlChangeListeners(this.prepareExternalUrl(e+mh(t)),i)}forward(){this._platformStrategy.forward()}back(){this._platformStrategy.back()}onUrlChange(e){this._urlChangeListeners.push(e),this._urlChangeSubscription||(this._urlChangeSubscription=this.subscribe(e=>{this._notifyUrlChangeListeners(e.url,e.state)}))}_notifyUrlChangeListeners(e="",t){this._urlChangeListeners.forEach(i=>i(e,t))}subscribe(e,t,i){return this._subject.subscribe({next:e,error:t,complete:i})}}return e.\u0275fac=function(t){return new(t||e)(mn(gh),mn(lh))},e.normalizeQueryParams=mh,e.joinWithSlash=ph,e.stripTrailingSlash=_h,e.\u0275prov=pe({factory:Sh,token:e,providedIn:"root"}),e})();function Sh(){return new wh(mn(gh),mn(lh))}function kh(e){return e.replace(/\/index.html$/,"")}var xh=function(e){return e[e.Zero=0]="Zero",e[e.One=1]="One",e[e.Two=2]="Two",e[e.Few=3]="Few",e[e.Many=4]="Many",e[e.Other=5]="Other",e}({});class Eh{}let Ah=(()=>{class e extends Eh{constructor(e){super(),this.locale=e}getPluralCategory(e,t){switch(function(e){return function(e){const t=function(e){return e.toLowerCase().replace(/_/g,"-")}(e);let i=ba(t);if(i)return i;const n=t.split("-")[0];if(i=ba(n),i)return i;if("en"===n)return va;throw new Error(`Missing locale data for the locale "${e}".`)}(e)[Ca.PluralCase]}(t||this.locale)(e)){case xh.Zero:return"zero";case xh.One:return"one";case xh.Two:return"two";case xh.Few:return"few";case xh.Many:return"many";default:return"other"}}}return e.\u0275fac=function(t){return new(t||e)(mn(mc))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();function Th(e,t){t=encodeURIComponent(t);for(const i of e.split(";")){const e=i.indexOf("="),[n,r]=-1==e?[i,""]:[i.slice(0,e),i.slice(e+1)];if(n.trim()===t)return decodeURIComponent(r)}return null}let Oh=(()=>{class e{constructor(e,t,i,n){this._iterableDiffers=e,this._keyValueDiffers=t,this._ngEl=i,this._renderer=n,this._iterableDiffer=null,this._keyValueDiffer=null,this._initialClasses=[],this._rawClass=null}set klass(e){this._removeClasses(this._initialClasses),this._initialClasses="string"==typeof e?e.split(/\s+/):[],this._applyClasses(this._initialClasses),this._applyClasses(this._rawClass)}set ngClass(e){this._removeClasses(this._rawClass),this._applyClasses(this._initialClasses),this._iterableDiffer=null,this._keyValueDiffer=null,this._rawClass="string"==typeof e?e.split(/\s+/):e,this._rawClass&&(go(this._rawClass)?this._iterableDiffer=this._iterableDiffers.find(this._rawClass).create():this._keyValueDiffer=this._keyValueDiffers.find(this._rawClass).create())}ngDoCheck(){if(this._iterableDiffer){const e=this._iterableDiffer.diff(this._rawClass);e&&this._applyIterableChanges(e)}else if(this._keyValueDiffer){const e=this._keyValueDiffer.diff(this._rawClass);e&&this._applyKeyValueChanges(e)}}_applyKeyValueChanges(e){e.forEachAddedItem(e=>this._toggleClass(e.key,e.currentValue)),e.forEachChangedItem(e=>this._toggleClass(e.key,e.currentValue)),e.forEachRemovedItem(e=>{e.previousValue&&this._toggleClass(e.key,!1)})}_applyIterableChanges(e){e.forEachAddedItem(e=>{if("string"!=typeof e.item)throw new Error("NgClass can only toggle CSS classes expressed as strings, got "+re(e.item));this._toggleClass(e.item,!0)}),e.forEachRemovedItem(e=>this._toggleClass(e.item,!1))}_applyClasses(e){e&&(Array.isArray(e)||e instanceof Set?e.forEach(e=>this._toggleClass(e,!0)):Object.keys(e).forEach(t=>this._toggleClass(t,!!e[t])))}_removeClasses(e){e&&(Array.isArray(e)||e instanceof Set?e.forEach(e=>this._toggleClass(e,!1)):Object.keys(e).forEach(e=>this._toggleClass(e,!1)))}_toggleClass(e,t){(e=e.trim())&&e.split(/\s+/g).forEach(e=>{t?this._renderer.addClass(this._ngEl.nativeElement,e):this._renderer.removeClass(this._ngEl.nativeElement,e)})}}return e.\u0275fac=function(t){return new(t||e)(xo(rl),xo(ol),xo(ja),xo(Ua))},e.\u0275dir=Qe({type:e,selectors:[["","ngClass",""]],inputs:{klass:["class","klass"],ngClass:"ngClass"}}),e})();class Rh{constructor(e,t,i,n){this.$implicit=e,this.ngForOf=t,this.index=i,this.count=n}get first(){return 0===this.index}get last(){return this.index===this.count-1}get even(){return this.index%2==0}get odd(){return!this.even}}let Lh=(()=>{class e{constructor(e,t,i){this._viewContainer=e,this._template=t,this._differs=i,this._ngForOf=null,this._ngForOfDirty=!0,this._differ=null}set ngForOf(e){this._ngForOf=e,this._ngForOfDirty=!0}set ngForTrackBy(e){this._trackByFn=e}get ngForTrackBy(){return this._trackByFn}set ngForTemplate(e){e&&(this._template=e)}ngDoCheck(){if(this._ngForOfDirty){this._ngForOfDirty=!1;const i=this._ngForOf;if(!this._differ&&i)try{this._differ=this._differs.find(i).create(this.ngForTrackBy)}catch(t){throw new Error(`Cannot find a differ supporting object '${i}' of type '${e=i,e.name||typeof e}'. NgFor only supports binding to Iterables such as Arrays.`)}}var e;if(this._differ){const e=this._differ.diff(this._ngForOf);e&&this._applyChanges(e)}}_applyChanges(e){const t=[];e.forEachOperation((e,i,n)=>{if(null==e.previousIndex){const i=this._viewContainer.createEmbeddedView(this._template,new Rh(null,this._ngForOf,-1,-1),null===n?void 0:n),r=new Dh(e,i);t.push(r)}else if(null==n)this._viewContainer.remove(null===i?void 0:i);else if(null!==i){const r=this._viewContainer.get(i);this._viewContainer.move(r,n);const s=new Dh(e,r);t.push(s)}});for(let i=0;i{this._viewContainer.get(e.currentIndex).context.$implicit=e.item})}_perViewChange(e,t){e.context.$implicit=t.item}static ngTemplateContextGuard(e,t){return!0}}return e.\u0275fac=function(t){return new(t||e)(xo(Sl),xo(ml),xo(rl))},e.\u0275dir=Qe({type:e,selectors:[["","ngFor","","ngForOf",""]],inputs:{ngForOf:"ngForOf",ngForTrackBy:"ngForTrackBy",ngForTemplate:"ngForTemplate"}}),e})();class Dh{constructor(e,t){this.record=e,this.view=t}}let Ph=(()=>{class e{constructor(e,t){this._viewContainer=e,this._context=new Ih,this._thenTemplateRef=null,this._elseTemplateRef=null,this._thenViewRef=null,this._elseViewRef=null,this._thenTemplateRef=t}set ngIf(e){this._context.$implicit=this._context.ngIf=e,this._updateView()}set ngIfThen(e){Mh("ngIfThen",e),this._thenTemplateRef=e,this._thenViewRef=null,this._updateView()}set ngIfElse(e){Mh("ngIfElse",e),this._elseTemplateRef=e,this._elseViewRef=null,this._updateView()}_updateView(){this._context.$implicit?this._thenViewRef||(this._viewContainer.clear(),this._elseViewRef=null,this._thenTemplateRef&&(this._thenViewRef=this._viewContainer.createEmbeddedView(this._thenTemplateRef,this._context))):this._elseViewRef||(this._viewContainer.clear(),this._thenViewRef=null,this._elseTemplateRef&&(this._elseViewRef=this._viewContainer.createEmbeddedView(this._elseTemplateRef,this._context)))}static ngTemplateContextGuard(e,t){return!0}}return e.\u0275fac=function(t){return new(t||e)(xo(Sl),xo(ml))},e.\u0275dir=Qe({type:e,selectors:[["","ngIf",""]],inputs:{ngIf:"ngIf",ngIfThen:"ngIfThen",ngIfElse:"ngIfElse"}}),e})();class Ih{constructor(){this.$implicit=null,this.ngIf=null}}function Mh(e,t){if(t&&!t.createEmbeddedView)throw new Error(`${e} must be a TemplateRef, but received '${re(t)}'.`)}class Fh{constructor(e,t){this._viewContainerRef=e,this._templateRef=t,this._created=!1}create(){this._created=!0,this._viewContainerRef.createEmbeddedView(this._templateRef)}destroy(){this._created=!1,this._viewContainerRef.clear()}enforceState(e){e&&!this._created?this.create():!e&&this._created&&this.destroy()}}let Hh=(()=>{class e{constructor(){this._defaultUsed=!1,this._caseCount=0,this._lastCaseCheckIndex=0,this._lastCasesMatched=!1}set ngSwitch(e){this._ngSwitch=e,0===this._caseCount&&this._updateDefaultCases(!0)}_addCase(){return this._caseCount++}_addDefault(e){this._defaultViews||(this._defaultViews=[]),this._defaultViews.push(e)}_matchCase(e){const t=e==this._ngSwitch;return this._lastCasesMatched=this._lastCasesMatched||t,this._lastCaseCheckIndex++,this._lastCaseCheckIndex===this._caseCount&&(this._updateDefaultCases(!this._lastCasesMatched),this._lastCaseCheckIndex=0,this._lastCasesMatched=!1),t}_updateDefaultCases(e){if(this._defaultViews&&e!==this._defaultUsed){this._defaultUsed=e;for(let t=0;t{class e{constructor(e,t,i){this.ngSwitch=i,i._addCase(),this._view=new Fh(e,t)}ngDoCheck(){this._view.enforceState(this.ngSwitch._matchCase(this.ngSwitchCase))}}return e.\u0275fac=function(t){return new(t||e)(xo(Sl),xo(ml),xo(Hh,1))},e.\u0275dir=Qe({type:e,selectors:[["","ngSwitchCase",""]],inputs:{ngSwitchCase:"ngSwitchCase"}}),e})(),jh=(()=>{class e{constructor(e,t,i){i._addDefault(new Fh(e,t))}}return e.\u0275fac=function(t){return new(t||e)(xo(Sl),xo(ml),xo(Hh,1))},e.\u0275dir=Qe({type:e,selectors:[["","ngSwitchDefault",""]]}),e})();class Nh{createSubscription(e,t){return e.subscribe({next:t,error:e=>{throw e}})}dispose(e){e.unsubscribe()}onDestroy(e){e.unsubscribe()}}class Vh{createSubscription(e,t){return e.then(t,e=>{throw e})}dispose(e){}onDestroy(e){}}const Uh=new Vh,qh=new Nh;let zh=(()=>{class e{constructor(e){this._ref=e,this._latestValue=null,this._subscription=null,this._obj=null,this._strategy=null}ngOnDestroy(){this._subscription&&this._dispose()}transform(e){return this._obj?e!==this._obj?(this._dispose(),this.transform(e)):this._latestValue:(e&&this._subscribe(e),this._latestValue)}_subscribe(e){this._obj=e,this._strategy=this._selectStrategy(e),this._subscription=this._strategy.createSubscription(e,t=>this._updateLatestValue(e,t))}_selectStrategy(t){if(Io(t))return Uh;if(Mo(t))return qh;throw Error(`InvalidPipeArgument: '${t}' for pipe '${re(e)}'`)}_dispose(){this._strategy.dispose(this._subscription),this._latestValue=null,this._subscription=null,this._obj=null}_updateLatestValue(e,t){e===this._obj&&(this._latestValue=t,this._ref.markForCheck())}}return e.\u0275fac=function(t){return new(t||e)(function(e=Se.Default){const t=ul(!0);if(null!=t||e&Se.Optional)return t;fe("ChangeDetectorRef")}())},e.\u0275pipe=Je({name:"async",type:e,pure:!1}),e})(),Wh=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[{provide:Eh,useClass:Ah}]}),e})();function $h(e){return"browser"===e}let Kh=(()=>{class e{}return e.\u0275prov=pe({token:e,providedIn:"root",factory:()=>new Gh(mn(ah),window)}),e})();class Gh{constructor(e,t){this.document=e,this.window=t,this.offset=()=>[0,0]}setOffset(e){this.offset=Array.isArray(e)?()=>e:e}getScrollPosition(){return this.supportsScrolling()?[this.window.pageXOffset,this.window.pageYOffset]:[0,0]}scrollToPosition(e){this.supportsScrolling()&&this.window.scrollTo(e[0],e[1])}scrollToAnchor(e){var t;if(!this.supportsScrolling())return;const i=null!==(t=this.document.getElementById(e))&&void 0!==t?t:this.document.getElementsByName(e)[0];void 0!==i&&(this.scrollToElement(i),this.attemptFocus(i))}setHistoryScrollRestoration(e){if(this.supportScrollRestoration()){const t=this.window.history;t&&t.scrollRestoration&&(t.scrollRestoration=e)}}scrollToElement(e){const t=e.getBoundingClientRect(),i=t.left+this.window.pageXOffset,n=t.top+this.window.pageYOffset,r=this.offset();this.window.scrollTo(i-r[0],n-r[1])}attemptFocus(e){return e.focus(),this.document.activeElement===e}supportScrollRestoration(){try{if(!this.supportsScrolling())return!1;const e=Zh(this.window.history)||Zh(Object.getPrototypeOf(this.window.history));return!(!e||!e.writable&&!e.set)}catch(e){return!1}}supportsScrolling(){try{return!!this.window&&!!this.window.scrollTo&&"pageXOffset"in this.window}catch(e){return!1}}}function Zh(e){return Object.getOwnPropertyDescriptor(e,"scrollRestoration")}class Yh extends class extends class{}{constructor(){super()}supportsDOMEvents(){return!0}}{static makeCurrent(){var e;e=new Yh,sh||(sh=e)}getProperty(e,t){return e[t]}log(e){window.console&&window.console.log&&window.console.log(e)}logGroup(e){window.console&&window.console.group&&window.console.group(e)}logGroupEnd(){window.console&&window.console.groupEnd&&window.console.groupEnd()}onAndCancel(e,t,i){return e.addEventListener(t,i,!1),()=>{e.removeEventListener(t,i,!1)}}dispatchEvent(e,t){e.dispatchEvent(t)}remove(e){return e.parentNode&&e.parentNode.removeChild(e),e}getValue(e){return e.value}createElement(e,t){return(t=t||this.getDefaultDocument()).createElement(e)}createHtmlDocument(){return document.implementation.createHTMLDocument("fakeTitle")}getDefaultDocument(){return document}isElementNode(e){return e.nodeType===Node.ELEMENT_NODE}isShadowRoot(e){return e instanceof DocumentFragment}getGlobalEventTarget(e,t){return"window"===t?window:"document"===t?e:"body"===t?e.body:null}getHistory(){return window.history}getLocation(){return window.location}getBaseHref(e){const t=Qh||(Qh=document.querySelector("base"),Qh)?Qh.getAttribute("href"):null;return null==t?null:(i=t,Xh||(Xh=document.createElement("a")),Xh.setAttribute("href",i),"/"===Xh.pathname.charAt(0)?Xh.pathname:"/"+Xh.pathname);var i}resetBaseElement(){Qh=null}getUserAgent(){return window.navigator.userAgent}performanceNow(){return window.performance&&window.performance.now?window.performance.now():(new Date).getTime()}supportsCookies(){return!0}getCookie(e){return Th(document.cookie,e)}}let Xh,Qh=null;const Jh=new Qi("TRANSITION_ID"),eu=[{provide:ac,useFactory:function(e,t,i){return()=>{i.get(lc).donePromise.then(()=>{const i=oh();Array.prototype.slice.apply(t.querySelectorAll("style[ng-transition]")).filter(t=>t.getAttribute("ng-transition")===e).forEach(e=>i.remove(e))})}},deps:[Jh,ah,oo],multi:!0}];class tu{static init(){var e;e=new tu,jc=e}addToWindow(e){Ie.getAngularTestability=(t,i=!0)=>{const n=e.findTestabilityInTree(t,i);if(null==n)throw new Error("Could not find testability for element.");return n},Ie.getAllAngularTestabilities=()=>e.getAllTestabilities(),Ie.getAllAngularRootElements=()=>e.getAllRootElements(),Ie.frameworkStabilizers||(Ie.frameworkStabilizers=[]),Ie.frameworkStabilizers.push(e=>{const t=Ie.getAllAngularTestabilities();let i=t.length,n=!1;const r=function(t){n=n||t,i--,0==i&&e(n)};t.forEach((function(e){e.whenStable(r)}))})}findTestabilityInTree(e,t,i){if(null==t)return null;const n=e.getTestability(t);return null!=n?n:i?oh().isShadowRoot(t)?this.findTestabilityInTree(e,t.host,!0):this.findTestabilityInTree(e,t.parentElement,!0):null}}const iu=new Qi("EventManagerPlugins");let nu=(()=>{class e{constructor(e,t){this._zone=t,this._eventNameToPlugin=new Map,e.forEach(e=>e.manager=this),this._plugins=e.slice().reverse()}addEventListener(e,t,i){return this._findPluginFor(t).addEventListener(e,t,i)}addGlobalEventListener(e,t,i){return this._findPluginFor(t).addGlobalEventListener(e,t,i)}getZone(){return this._zone}_findPluginFor(e){const t=this._eventNameToPlugin.get(e);if(t)return t;const i=this._plugins;for(let n=0;n{class e{constructor(){this._stylesSet=new Set}addStyles(e){const t=new Set;e.forEach(e=>{this._stylesSet.has(e)||(this._stylesSet.add(e),t.add(e))}),this.onStylesAdded(t)}onStylesAdded(e){}getAllStyles(){return Array.from(this._stylesSet)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),ou=(()=>{class e extends su{constructor(e){super(),this._doc=e,this._hostNodes=new Set,this._styleNodes=new Set,this._hostNodes.add(e.head)}_addStylesToHost(e,t){e.forEach(e=>{const i=this._doc.createElement("style");i.textContent=e,this._styleNodes.add(t.appendChild(i))})}addHost(e){this._addStylesToHost(this._stylesSet,e),this._hostNodes.add(e)}removeHost(e){this._hostNodes.delete(e)}onStylesAdded(e){this._hostNodes.forEach(t=>this._addStylesToHost(e,t))}ngOnDestroy(){this._styleNodes.forEach(e=>oh().remove(e))}}return e.\u0275fac=function(t){return new(t||e)(mn(ah))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const au={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"},lu=/%COMP%/g;function cu(e,t,i){for(let n=0;n{if("__ngUnwrap__"===t)return e;!1===e(t)&&(t.preventDefault(),t.returnValue=!1)}}let uu=(()=>{class e{constructor(e,t,i){this.eventManager=e,this.sharedStylesHost=t,this.appId=i,this.rendererByCompId=new Map,this.defaultRenderer=new du(e)}createRenderer(e,t){if(!e||!t)return this.defaultRenderer;switch(t.encapsulation){case Oe.Emulated:{let i=this.rendererByCompId.get(t.id);return i||(i=new fu(this.eventManager,this.sharedStylesHost,t,this.appId),this.rendererByCompId.set(t.id,i)),i.applyToHost(e),i}case 1:case Oe.ShadowDom:return new pu(this.eventManager,this.sharedStylesHost,e,t);default:if(!this.rendererByCompId.has(t.id)){const e=cu(t.id,t.styles,[]);this.sharedStylesHost.addStyles(e),this.rendererByCompId.set(t.id,this.defaultRenderer)}return this.defaultRenderer}}begin(){}end(){}}return e.\u0275fac=function(t){return new(t||e)(mn(nu),mn(ou),mn(cc))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();class du{constructor(e){this.eventManager=e,this.data=Object.create(null)}destroy(){}createElement(e,t){return t?document.createElementNS(au[t]||t,e):document.createElement(e)}createComment(e){return document.createComment(e)}createText(e){return document.createTextNode(e)}appendChild(e,t){e.appendChild(t)}insertBefore(e,t,i){e&&e.insertBefore(t,i)}removeChild(e,t){e&&e.removeChild(t)}selectRootElement(e,t){let i="string"==typeof e?document.querySelector(e):e;if(!i)throw new Error(`The selector "${e}" did not match any elements`);return t||(i.textContent=""),i}parentNode(e){return e.parentNode}nextSibling(e){return e.nextSibling}setAttribute(e,t,i,n){if(n){t=n+":"+t;const r=au[n];r?e.setAttributeNS(r,t,i):e.setAttribute(t,i)}else e.setAttribute(t,i)}removeAttribute(e,t,i){if(i){const n=au[i];n?e.removeAttributeNS(n,t):e.removeAttribute(`${i}:${t}`)}else e.removeAttribute(t)}addClass(e,t){e.classList.add(t)}removeClass(e,t){e.classList.remove(t)}setStyle(e,t,i,n){n&(hr.DashCase|hr.Important)?e.style.setProperty(t,i,n&hr.Important?"important":""):e.style[t]=i}removeStyle(e,t,i){i&hr.DashCase?e.style.removeProperty(t):e.style[t]=""}setProperty(e,t,i){e[t]=i}setValue(e,t){e.nodeValue=t}listen(e,t,i){return"string"==typeof e?this.eventManager.addGlobalEventListener(e,t,hu(i)):this.eventManager.addEventListener(e,t,hu(i))}}class fu extends du{constructor(e,t,i,n){super(e),this.component=i;const r=cu(n+"-"+i.id,i.styles,[]);t.addStyles(r),this.contentAttr="_ngcontent-%COMP%".replace(lu,n+"-"+i.id),this.hostAttr="_nghost-%COMP%".replace(lu,n+"-"+i.id)}applyToHost(e){super.setAttribute(e,this.hostAttr,"")}createElement(e,t){const i=super.createElement(e,t);return super.setAttribute(i,this.contentAttr,""),i}}class pu extends du{constructor(e,t,i,n){super(e),this.sharedStylesHost=t,this.hostEl=i,this.shadowRoot=i.attachShadow({mode:"open"}),this.sharedStylesHost.addHost(this.shadowRoot);const r=cu(n.id,n.styles,[]);for(let s=0;s{class e extends ru{constructor(e){super(e)}supports(e){return!0}addEventListener(e,t,i){return e.addEventListener(t,i,!1),()=>this.removeEventListener(e,t,i)}removeEventListener(e,t,i){return e.removeEventListener(t,i)}}return e.\u0275fac=function(t){return new(t||e)(mn(ah))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const mu=["alt","control","meta","shift"],gu={"\b":"Backspace","\t":"Tab","\x7f":"Delete","\x1b":"Escape",Del:"Delete",Esc:"Escape",Left:"ArrowLeft",Right:"ArrowRight",Up:"ArrowUp",Down:"ArrowDown",Menu:"ContextMenu",Scroll:"ScrollLock",Win:"OS"},vu={A:"1",B:"2",C:"3",D:"4",E:"5",F:"6",G:"7",H:"8",I:"9",J:"*",K:"+",M:"-",N:".",O:"/","`":"0","\x90":"NumLock"},yu={alt:e=>e.altKey,control:e=>e.ctrlKey,meta:e=>e.metaKey,shift:e=>e.shiftKey};let bu=(()=>{class e extends ru{constructor(e){super(e)}supports(t){return null!=e.parseEventName(t)}addEventListener(t,i,n){const r=e.parseEventName(i),s=e.eventCallback(r.fullKey,n,this.manager.getZone());return this.manager.getZone().runOutsideAngular(()=>oh().onAndCancel(t,r.domEventName,s))}static parseEventName(t){const i=t.toLowerCase().split("."),n=i.shift();if(0===i.length||"keydown"!==n&&"keyup"!==n)return null;const r=e._normalizeKey(i.pop());let s="";if(mu.forEach(e=>{const t=i.indexOf(e);t>-1&&(i.splice(t,1),s+=e+".")}),s+=r,0!=i.length||0===r.length)return null;const o={};return o.domEventName=n,o.fullKey=s,o}static getEventFullKey(e){let t="",i=function(e){let t=e.key;if(null==t){if(t=e.keyIdentifier,null==t)return"Unidentified";t.startsWith("U+")&&(t=String.fromCharCode(parseInt(t.substring(2),16)),3===e.location&&vu.hasOwnProperty(t)&&(t=vu[t]))}return gu[t]||t}(e);return i=i.toLowerCase()," "===i?i="space":"."===i&&(i="dot"),mu.forEach(n=>{n!=i&&(0,yu[n])(e)&&(t+=n+".")}),t+=i,t}static eventCallback(t,i,n){return r=>{e.getEventFullKey(r)===t&&n.runGuarded(()=>i(r))}}static _normalizeKey(e){switch(e){case"esc":return"escape";default:return e}}}return e.\u0275fac=function(t){return new(t||e)(mn(ah))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const Cu=Wc(ih,"browser",[{provide:fc,useValue:"browser"},{provide:dc,useValue:function(){Yh.makeCurrent(),tu.init()},multi:!0},{provide:ah,useFactory:function(){return function(e){vt=e}(document),document},deps:[]}]),wu=[[],{provide:$s,useValue:"root"},{provide:sr,useFactory:function(){return new sr},deps:[]},{provide:iu,useClass:_u,multi:!0,deps:[ah,Tc,fc]},{provide:iu,useClass:bu,multi:!0,deps:[ah]},[],{provide:uu,useClass:uu,deps:[nu,ou,cc]},{provide:Va,useExisting:uu},{provide:su,useExisting:ou},{provide:ou,useClass:ou,deps:[ah]},{provide:Mc,useClass:Mc,deps:[Tc]},{provide:nu,useClass:nu,deps:[iu,Tc]},[]];let Su=(()=>{class e{constructor(e){if(e)throw new Error("BrowserModule has already been loaded. If you need access to common directives such as NgIf and NgFor from a lazy loaded module, import CommonModule instead.")}static withServerTransition(t){return{ngModule:e,providers:[{provide:cc,useValue:t.appId},{provide:Jh,useExisting:cc},eu]}}}return e.\u0275fac=function(t){return new(t||e)(mn(e,12))},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:wu,imports:[Wh,rh]}),e})();"undefined"!=typeof window&&window;class ku{}const xu="*";function Eu(e,t){return{type:7,name:e,definitions:t,options:{}}}function Au(e,t=null){return{type:4,styles:t,timings:e}}function Tu(e,t=null){return{type:2,steps:e,options:t}}function Ou(e){return{type:6,styles:e,offset:null}}function Ru(e,t,i){return{type:0,name:e,styles:t,options:i}}function Lu(e,t,i=null){return{type:1,expr:e,animation:t,options:i}}function Du(e=null){return{type:9,options:e}}function Pu(e,t,i=null){return{type:11,selector:e,animation:t,options:i}}function Iu(e){Promise.resolve(null).then(e)}class Mu{constructor(e=0,t=0){this._onDoneFns=[],this._onStartFns=[],this._onDestroyFns=[],this._started=!1,this._destroyed=!1,this._finished=!1,this._position=0,this.parentPlayer=null,this.totalTime=e+t}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(e=>e()),this._onDoneFns=[])}onStart(e){this._onStartFns.push(e)}onDone(e){this._onDoneFns.push(e)}onDestroy(e){this._onDestroyFns.push(e)}hasStarted(){return this._started}init(){}play(){this.hasStarted()||(this._onStart(),this.triggerMicrotask()),this._started=!0}triggerMicrotask(){Iu(()=>this._onFinish())}_onStart(){this._onStartFns.forEach(e=>e()),this._onStartFns=[]}pause(){}restart(){}finish(){this._onFinish()}destroy(){this._destroyed||(this._destroyed=!0,this.hasStarted()||this._onStart(),this.finish(),this._onDestroyFns.forEach(e=>e()),this._onDestroyFns=[])}reset(){}setPosition(e){this._position=this.totalTime?e*this.totalTime:1}getPosition(){return this.totalTime?this._position/this.totalTime:1}triggerCallback(e){const t="start"==e?this._onStartFns:this._onDoneFns;t.forEach(e=>e()),t.length=0}}class Fu{constructor(e){this._onDoneFns=[],this._onStartFns=[],this._finished=!1,this._started=!1,this._destroyed=!1,this._onDestroyFns=[],this.parentPlayer=null,this.totalTime=0,this.players=e;let t=0,i=0,n=0;const r=this.players.length;0==r?Iu(()=>this._onFinish()):this.players.forEach(e=>{e.onDone(()=>{++t==r&&this._onFinish()}),e.onDestroy(()=>{++i==r&&this._onDestroy()}),e.onStart(()=>{++n==r&&this._onStart()})}),this.totalTime=this.players.reduce((e,t)=>Math.max(e,t.totalTime),0)}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(e=>e()),this._onDoneFns=[])}init(){this.players.forEach(e=>e.init())}onStart(e){this._onStartFns.push(e)}_onStart(){this.hasStarted()||(this._started=!0,this._onStartFns.forEach(e=>e()),this._onStartFns=[])}onDone(e){this._onDoneFns.push(e)}onDestroy(e){this._onDestroyFns.push(e)}hasStarted(){return this._started}play(){this.parentPlayer||this.init(),this._onStart(),this.players.forEach(e=>e.play())}pause(){this.players.forEach(e=>e.pause())}restart(){this.players.forEach(e=>e.restart())}finish(){this._onFinish(),this.players.forEach(e=>e.finish())}destroy(){this._onDestroy()}_onDestroy(){this._destroyed||(this._destroyed=!0,this._onFinish(),this.players.forEach(e=>e.destroy()),this._onDestroyFns.forEach(e=>e()),this._onDestroyFns=[])}reset(){this.players.forEach(e=>e.reset()),this._destroyed=!1,this._finished=!1,this._started=!1}setPosition(e){const t=e*this.totalTime;this.players.forEach(e=>{const i=e.totalTime?Math.min(1,t/e.totalTime):1;e.setPosition(i)})}getPosition(){const e=this.players.reduce((e,t)=>null===e||t.totalTime>e.totalTime?t:e,null);return null!=e?e.getPosition():0}beforeDestroy(){this.players.forEach(e=>{e.beforeDestroy&&e.beforeDestroy()})}triggerCallback(e){const t="start"==e?this._onStartFns:this._onDoneFns;t.forEach(e=>e()),t.length=0}}function Hu(){return"undefined"!=typeof process&&"[object process]"==={}.toString.call(process)}function Bu(e){switch(e.length){case 0:return new Mu;case 1:return e[0];default:return new Fu(e)}}function ju(e,t,i,n,r={},s={}){const o=[],a=[];let l=-1,c=null;if(n.forEach(e=>{const i=e.offset,n=i==l,h=n&&c||{};Object.keys(e).forEach(i=>{let n=i,a=e[i];if("offset"!==i)switch(n=t.normalizePropertyName(n,o),a){case"!":a=r[i];break;case xu:a=s[i];break;default:a=t.normalizeStyleValue(i,n,a,o)}h[n]=a}),n||a.push(h),c=h,l=i}),o.length){const e="\n - ";throw new Error(`Unable to animate due to the following errors:${e}${o.join(e)}`)}return a}function Nu(e,t,i,n){switch(t){case"start":e.onStart(()=>n(i&&Vu(i,"start",e)));break;case"done":e.onDone(()=>n(i&&Vu(i,"done",e)));break;case"destroy":e.onDestroy(()=>n(i&&Vu(i,"destroy",e)))}}function Vu(e,t,i){const n=i.totalTime,r=Uu(e.element,e.triggerName,e.fromState,e.toState,t||e.phaseName,null==n?e.totalTime:n,!!i.disabled),s=e._data;return null!=s&&(r._data=s),r}function Uu(e,t,i,n,r="",s=0,o){return{element:e,triggerName:t,fromState:i,toState:n,phaseName:r,totalTime:s,disabled:!!o}}function qu(e,t,i){let n;return e instanceof Map?(n=e.get(t),n||e.set(t,n=i)):(n=e[t],n||(n=e[t]=i)),n}function zu(e){const t=e.indexOf(":");return[e.substring(1,t),e.substr(t+1)]}let Wu=(e,t)=>!1,$u=(e,t)=>!1,Ku=(e,t,i)=>[];const Gu=Hu();(Gu||"undefined"!=typeof Element)&&(Wu=(e,t)=>e.contains(t),$u=(()=>{if(Gu||Element.prototype.matches)return(e,t)=>e.matches(t);{const e=Element.prototype,t=e.matchesSelector||e.mozMatchesSelector||e.msMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;return t?(e,i)=>t.apply(e,[i]):$u}})(),Ku=(e,t,i)=>{let n=[];if(i){const i=e.querySelectorAll(t);for(let e=0;e{const n=i.replace(/([a-z])([A-Z])/g,"$1-$2");t[n]=e[i]}),t}let id=(()=>{class e{validateStyleProperty(e){return Xu(e)}matchesElement(e,t){return Qu(e,t)}containsElement(e,t){return Ju(e,t)}query(e,t,i){return ed(e,t,i)}computeStyle(e,t,i){return i||""}animate(e,t,i,n,r,s=[],o){return new Mu(i,n)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),nd=(()=>{class e{}return e.NOOP=new id,e})();const rd="ng-enter",sd="ng-leave",od="ng-trigger",ad=".ng-trigger",ld="ng-animating",cd=".ng-animating";function hd(e){if("number"==typeof e)return e;const t=e.match(/^(-?[\.\d]+)(m?s)/);return!t||t.length<2?0:ud(parseFloat(t[1]),t[2])}function ud(e,t){switch(t){case"s":return 1e3*e;default:return e}}function dd(e,t,i){return e.hasOwnProperty("duration")?e:function(e,t,i){let n,r=0,s="";if("string"==typeof e){const i=e.match(/^(-?[\.\d]+)(m?s)(?:\s+(-?[\.\d]+)(m?s))?(?:\s+([-a-z]+(?:\(.+?\))?))?$/i);if(null===i)return t.push(`The provided timing value "${e}" is invalid.`),{duration:0,delay:0,easing:""};n=ud(parseFloat(i[1]),i[2]);const o=i[3];null!=o&&(r=ud(parseFloat(o),i[4]));const a=i[5];a&&(s=a)}else n=e;if(!i){let i=!1,s=t.length;n<0&&(t.push("Duration values below 0 are not allowed for this animation step."),i=!0),r<0&&(t.push("Delay values below 0 are not allowed for this animation step."),i=!0),i&&t.splice(s,0,`The provided timing value "${e}" is invalid.`)}return{duration:n,delay:r,easing:s}}(e,t,i)}function fd(e,t={}){return Object.keys(e).forEach(i=>{t[i]=e[i]}),t}function pd(e,t,i={}){if(t)for(let n in e)i[n]=e[n];else fd(e,i);return i}function _d(e,t,i){return i?t+":"+i+";":""}function md(e){let t="";for(let i=0;i{const r=xd(n);i&&!i.hasOwnProperty(n)&&(i[n]=e.style[r]),e.style[r]=t[n]}),Hu()&&md(e))}function vd(e,t){e.style&&(Object.keys(t).forEach(t=>{const i=xd(t);e.style[i]=""}),Hu()&&md(e))}function yd(e){return Array.isArray(e)?1==e.length?e[0]:Tu(e):e}const bd=new RegExp("{{\\s*(.+?)\\s*}}","g");function Cd(e){let t=[];if("string"==typeof e){let i;for(;i=bd.exec(e);)t.push(i[1]);bd.lastIndex=0}return t}function wd(e,t,i){const n=e.toString(),r=n.replace(bd,(e,n)=>{let r=t[n];return t.hasOwnProperty(n)||(i.push("Please provide a value for the animation param "+n),r=""),r.toString()});return r==n?e:r}function Sd(e){const t=[];let i=e.next();for(;!i.done;)t.push(i.value),i=e.next();return t}const kd=/-+([a-z0-9])/g;function xd(e){return e.replace(kd,(...e)=>e[1].toUpperCase())}function Ed(e,t){return 0===e||0===t}function Ad(e,t,i){const n=Object.keys(i);if(n.length&&t.length){let s=t[0],o=[];if(n.forEach(e=>{s.hasOwnProperty(e)||o.push(e),s[e]=i[e]}),o.length)for(var r=1;rfunction(e,t,i){if(":"==e[0]){const n=function(e,t){switch(e){case":enter":return"void => *";case":leave":return"* => void";case":increment":return(e,t)=>parseFloat(t)>parseFloat(e);case":decrement":return(e,t)=>parseFloat(t) *"}}(e,i);if("function"==typeof n)return void t.push(n);e=n}const n=e.match(/^(\*|[-\w]+)\s*([=-]>)\s*(\*|[-\w]+)$/);if(null==n||n.length<4)return i.push(`The provided transition expression "${e}" is not supported`),t;const r=n[1],s=n[2],o=n[3];t.push(Id(r,o)),"<"!=s[0]||r==Rd&&o==Rd||t.push(Id(o,r))}(e,i,t)):i.push(e),i}const Dd=new Set(["true","1"]),Pd=new Set(["false","0"]);function Id(e,t){const i=Dd.has(e)||Pd.has(e),n=Dd.has(t)||Pd.has(t);return(r,s)=>{let o=e==Rd||e==r,a=t==Rd||t==s;return!o&&i&&"boolean"==typeof r&&(o=r?Dd.has(e):Pd.has(e)),!a&&n&&"boolean"==typeof s&&(a=s?Dd.has(t):Pd.has(t)),o&&a}}const Md=new RegExp("s*:selfs*,?","g");function Fd(e,t,i){return new Hd(e).build(t,i)}class Hd{constructor(e){this._driver=e}build(e,t){const i=new Bd(t);return this._resetContextStyleTimingState(i),Td(this,yd(e),i)}_resetContextStyleTimingState(e){e.currentQuerySelector="",e.collectedStyles={},e.collectedStyles[""]={},e.currentTime=0}visitTrigger(e,t){let i=t.queryCount=0,n=t.depCount=0;const r=[],s=[];return"@"==e.name.charAt(0)&&t.errors.push("animation triggers cannot be prefixed with an `@` sign (e.g. trigger('@foo', [...]))"),e.definitions.forEach(e=>{if(this._resetContextStyleTimingState(t),0==e.type){const i=e,n=i.name;n.toString().split(/\s*,\s*/).forEach(e=>{i.name=e,r.push(this.visitState(i,t))}),i.name=n}else if(1==e.type){const r=this.visitTransition(e,t);i+=r.queryCount,n+=r.depCount,s.push(r)}else t.errors.push("only state() and transition() definitions can sit inside of a trigger()")}),{type:7,name:e.name,states:r,transitions:s,queryCount:i,depCount:n,options:null}}visitState(e,t){const i=this.visitStyle(e.styles,t),n=e.options&&e.options.params||null;if(i.containsDynamicStyles){const r=new Set,s=n||{};if(i.styles.forEach(e=>{if(jd(e)){const t=e;Object.keys(t).forEach(e=>{Cd(t[e]).forEach(e=>{s.hasOwnProperty(e)||r.add(e)})})}}),r.size){const i=Sd(r.values());t.errors.push(`state("${e.name}", ...) must define default values for all the following style substitutions: ${i.join(", ")}`)}}return{type:0,name:e.name,style:i,options:n?{params:n}:null}}visitTransition(e,t){t.queryCount=0,t.depCount=0;const i=Td(this,yd(e.animation),t);return{type:1,matchers:Ld(e.expr,t.errors),animation:i,queryCount:t.queryCount,depCount:t.depCount,options:Nd(e.options)}}visitSequence(e,t){return{type:2,steps:e.steps.map(e=>Td(this,e,t)),options:Nd(e.options)}}visitGroup(e,t){const i=t.currentTime;let n=0;const r=e.steps.map(e=>{t.currentTime=i;const r=Td(this,e,t);return n=Math.max(n,t.currentTime),r});return t.currentTime=n,{type:3,steps:r,options:Nd(e.options)}}visitAnimate(e,t){const i=function(e,t){let i=null;if(e.hasOwnProperty("duration"))i=e;else if("number"==typeof e)return Vd(dd(e,t).duration,0,"");const n=e;if(n.split(/\s+/).some(e=>"{"==e.charAt(0)&&"{"==e.charAt(1))){const e=Vd(0,0,"");return e.dynamic=!0,e.strValue=n,e}return i=i||dd(n,t),Vd(i.duration,i.delay,i.easing)}(e.timings,t.errors);let n;t.currentAnimateTimings=i;let r=e.styles?e.styles:Ou({});if(5==r.type)n=this.visitKeyframes(r,t);else{let r=e.styles,s=!1;if(!r){s=!0;const e={};i.easing&&(e.easing=i.easing),r=Ou(e)}t.currentTime+=i.duration+i.delay;const o=this.visitStyle(r,t);o.isEmptyStep=s,n=o}return t.currentAnimateTimings=null,{type:4,timings:i,style:n,options:null}}visitStyle(e,t){const i=this._makeStyleAst(e,t);return this._validateStyleAst(i,t),i}_makeStyleAst(e,t){const i=[];Array.isArray(e.styles)?e.styles.forEach(e=>{"string"==typeof e?e==xu?i.push(e):t.errors.push(`The provided style string value ${e} is not allowed.`):i.push(e)}):i.push(e.styles);let n=!1,r=null;return i.forEach(e=>{if(jd(e)){const t=e,i=t.easing;if(i&&(r=i,delete t.easing),!n)for(let e in t)if(t[e].toString().indexOf("{{")>=0){n=!0;break}}}),{type:6,styles:i,easing:r,offset:e.offset,containsDynamicStyles:n,options:null}}_validateStyleAst(e,t){const i=t.currentAnimateTimings;let n=t.currentTime,r=t.currentTime;i&&r>0&&(r-=i.duration+i.delay),e.styles.forEach(e=>{"string"!=typeof e&&Object.keys(e).forEach(i=>{if(!this._driver.validateStyleProperty(i))return void t.errors.push(`The provided animation property "${i}" is not a supported CSS property for animations`);const s=t.collectedStyles[t.currentQuerySelector],o=s[i];let a=!0;o&&(r!=n&&r>=o.startTime&&n<=o.endTime&&(t.errors.push(`The CSS property "${i}" that exists between the times of "${o.startTime}ms" and "${o.endTime}ms" is also being animated in a parallel animation between the times of "${r}ms" and "${n}ms"`),a=!1),r=o.startTime),a&&(s[i]={startTime:r,endTime:n}),t.options&&function(e,t,i){const n=t.params||{},r=Cd(e);r.length&&r.forEach(e=>{n.hasOwnProperty(e)||i.push(`Unable to resolve the local animation param ${e} in the given list of values`)})}(e[i],t.options,t.errors)})})}visitKeyframes(e,t){const i={type:5,styles:[],options:null};if(!t.currentAnimateTimings)return t.errors.push("keyframes() must be placed inside of a call to animate()"),i;let n=0;const r=[];let s=!1,o=!1,a=0;const l=e.steps.map(e=>{const i=this._makeStyleAst(e,t);let l=null!=i.offset?i.offset:function(e){if("string"==typeof e)return null;let t=null;if(Array.isArray(e))e.forEach(e=>{if(jd(e)&&e.hasOwnProperty("offset")){const i=e;t=parseFloat(i.offset),delete i.offset}});else if(jd(e)&&e.hasOwnProperty("offset")){const i=e;t=parseFloat(i.offset),delete i.offset}return t}(i.styles),c=0;return null!=l&&(n++,c=i.offset=l),o=o||c<0||c>1,s=s||c0&&n{const s=h>0?n==u?1:h*n:r[n],o=s*p;t.currentTime=d+f.delay+o,f.duration=o,this._validateStyleAst(e,t),e.offset=s,i.styles.push(e)}),i}visitReference(e,t){return{type:8,animation:Td(this,yd(e.animation),t),options:Nd(e.options)}}visitAnimateChild(e,t){return t.depCount++,{type:9,options:Nd(e.options)}}visitAnimateRef(e,t){return{type:10,animation:this.visitReference(e.animation,t),options:Nd(e.options)}}visitQuery(e,t){const i=t.currentQuerySelector,n=e.options||{};t.queryCount++,t.currentQuery=e;const[r,s]=function(e){const t=!!e.split(/\s*,\s*/).find(e=>":self"==e);return t&&(e=e.replace(Md,"")),[e=e.replace(/@\*/g,ad).replace(/@\w+/g,e=>".ng-trigger-"+e.substr(1)).replace(/:animating/g,cd),t]}(e.selector);t.currentQuerySelector=i.length?i+" "+r:r,qu(t.collectedStyles,t.currentQuerySelector,{});const o=Td(this,yd(e.animation),t);return t.currentQuery=null,t.currentQuerySelector=i,{type:11,selector:r,limit:n.limit||0,optional:!!n.optional,includeSelf:s,animation:o,originalSelector:e.selector,options:Nd(e.options)}}visitStagger(e,t){t.currentQuery||t.errors.push("stagger() can only be used inside of query()");const i="full"===e.timings?{duration:0,delay:0,easing:"full"}:dd(e.timings,t.errors,!0);return{type:12,animation:Td(this,yd(e.animation),t),timings:i,options:null}}}class Bd{constructor(e){this.errors=e,this.queryCount=0,this.depCount=0,this.currentTransition=null,this.currentQuery=null,this.currentQuerySelector=null,this.currentAnimateTimings=null,this.currentTime=0,this.collectedStyles={},this.options=null}}function jd(e){return!Array.isArray(e)&&"object"==typeof e}function Nd(e){var t;return e?(e=fd(e)).params&&(e.params=(t=e.params)?fd(t):null):e={},e}function Vd(e,t,i){return{duration:e,delay:t,easing:i}}function Ud(e,t,i,n,r,s,o=null,a=!1){return{type:1,element:e,keyframes:t,preStyleProps:i,postStyleProps:n,duration:r,delay:s,totalTime:r+s,easing:o,subTimeline:a}}class qd{constructor(){this._map=new Map}consume(e){let t=this._map.get(e);return t?this._map.delete(e):t=[],t}append(e,t){let i=this._map.get(e);i||this._map.set(e,i=[]),i.push(...t)}has(e){return this._map.has(e)}clear(){this._map.clear()}}const zd=new RegExp(":enter","g"),Wd=new RegExp(":leave","g");function $d(e,t,i,n,r,s={},o={},a,l,c=[]){return(new Kd).buildKeyframes(e,t,i,n,r,s,o,a,l,c)}class Kd{buildKeyframes(e,t,i,n,r,s,o,a,l,c=[]){l=l||new qd;const h=new Zd(e,t,l,n,r,c,[]);h.options=a,h.currentTimeline.setStyles([s],null,h.errors,a),Td(this,i,h);const u=h.timelines.filter(e=>e.containsAnimation());if(u.length&&Object.keys(o).length){const e=u[u.length-1];e.allowOnlyTimelineStyles()||e.setStyles([o],null,h.errors,a)}return u.length?u.map(e=>e.buildKeyframes()):[Ud(t,[],[],[],0,0,"",!1)]}visitTrigger(e,t){}visitState(e,t){}visitTransition(e,t){}visitAnimateChild(e,t){const i=t.subInstructions.consume(t.element);if(i){const n=t.createSubContext(e.options),r=t.currentTimeline.currentTime,s=this._visitSubInstructions(i,n,n.options);r!=s&&t.transformIntoNewTimeline(s)}t.previousNode=e}visitAnimateRef(e,t){const i=t.createSubContext(e.options);i.transformIntoNewTimeline(),this.visitReference(e.animation,i),t.transformIntoNewTimeline(i.currentTimeline.currentTime),t.previousNode=e}_visitSubInstructions(e,t,i){let n=t.currentTimeline.currentTime;const r=null!=i.duration?hd(i.duration):null,s=null!=i.delay?hd(i.delay):null;return 0!==r&&e.forEach(e=>{const i=t.appendInstructionToTimeline(e,r,s);n=Math.max(n,i.duration+i.delay)}),n}visitReference(e,t){t.updateOptions(e.options,!0),Td(this,e.animation,t),t.previousNode=e}visitSequence(e,t){const i=t.subContextCount;let n=t;const r=e.options;if(r&&(r.params||r.delay)&&(n=t.createSubContext(r),n.transformIntoNewTimeline(),null!=r.delay)){6==n.previousNode.type&&(n.currentTimeline.snapshotCurrentStyles(),n.previousNode=Gd);const e=hd(r.delay);n.delayNextStep(e)}e.steps.length&&(e.steps.forEach(e=>Td(this,e,n)),n.currentTimeline.applyStylesToKeyframe(),n.subContextCount>i&&n.transformIntoNewTimeline()),t.previousNode=e}visitGroup(e,t){const i=[];let n=t.currentTimeline.currentTime;const r=e.options&&e.options.delay?hd(e.options.delay):0;e.steps.forEach(s=>{const o=t.createSubContext(e.options);r&&o.delayNextStep(r),Td(this,s,o),n=Math.max(n,o.currentTimeline.currentTime),i.push(o.currentTimeline)}),i.forEach(e=>t.currentTimeline.mergeTimelineCollectedStyles(e)),t.transformIntoNewTimeline(n),t.previousNode=e}_visitTiming(e,t){if(e.dynamic){const i=e.strValue;return dd(t.params?wd(i,t.params,t.errors):i,t.errors)}return{duration:e.duration,delay:e.delay,easing:e.easing}}visitAnimate(e,t){const i=t.currentAnimateTimings=this._visitTiming(e.timings,t),n=t.currentTimeline;i.delay&&(t.incrementTime(i.delay),n.snapshotCurrentStyles());const r=e.style;5==r.type?this.visitKeyframes(r,t):(t.incrementTime(i.duration),this.visitStyle(r,t),n.applyStylesToKeyframe()),t.currentAnimateTimings=null,t.previousNode=e}visitStyle(e,t){const i=t.currentTimeline,n=t.currentAnimateTimings;!n&&i.getCurrentStyleProperties().length&&i.forwardFrame();const r=n&&n.easing||e.easing;e.isEmptyStep?i.applyEmptyStep(r):i.setStyles(e.styles,r,t.errors,t.options),t.previousNode=e}visitKeyframes(e,t){const i=t.currentAnimateTimings,n=t.currentTimeline.duration,r=i.duration,s=t.createSubContext().currentTimeline;s.easing=i.easing,e.styles.forEach(e=>{s.forwardTime((e.offset||0)*r),s.setStyles(e.styles,e.easing,t.errors,t.options),s.applyStylesToKeyframe()}),t.currentTimeline.mergeTimelineCollectedStyles(s),t.transformIntoNewTimeline(n+r),t.previousNode=e}visitQuery(e,t){const i=t.currentTimeline.currentTime,n=e.options||{},r=n.delay?hd(n.delay):0;r&&(6===t.previousNode.type||0==i&&t.currentTimeline.getCurrentStyleProperties().length)&&(t.currentTimeline.snapshotCurrentStyles(),t.previousNode=Gd);let s=i;const o=t.invokeQuery(e.selector,e.originalSelector,e.limit,e.includeSelf,!!n.optional,t.errors);t.currentQueryTotal=o.length;let a=null;o.forEach((i,n)=>{t.currentQueryIndex=n;const o=t.createSubContext(e.options,i);r&&o.delayNextStep(r),i===t.element&&(a=o.currentTimeline),Td(this,e.animation,o),o.currentTimeline.applyStylesToKeyframe(),s=Math.max(s,o.currentTimeline.currentTime)}),t.currentQueryIndex=0,t.currentQueryTotal=0,t.transformIntoNewTimeline(s),a&&(t.currentTimeline.mergeTimelineCollectedStyles(a),t.currentTimeline.snapshotCurrentStyles()),t.previousNode=e}visitStagger(e,t){const i=t.parentContext,n=t.currentTimeline,r=e.timings,s=Math.abs(r.duration),o=s*(t.currentQueryTotal-1);let a=s*t.currentQueryIndex;switch(r.duration<0?"reverse":r.easing){case"reverse":a=o-a;break;case"full":a=i.currentStaggerTime}const l=t.currentTimeline;a&&l.delayNextStep(a);const c=l.currentTime;Td(this,e.animation,t),t.previousNode=e,i.currentStaggerTime=n.currentTime-c+(n.startTime-i.currentTimeline.startTime)}}const Gd={};class Zd{constructor(e,t,i,n,r,s,o,a){this._driver=e,this.element=t,this.subInstructions=i,this._enterClassName=n,this._leaveClassName=r,this.errors=s,this.timelines=o,this.parentContext=null,this.currentAnimateTimings=null,this.previousNode=Gd,this.subContextCount=0,this.options={},this.currentQueryIndex=0,this.currentQueryTotal=0,this.currentStaggerTime=0,this.currentTimeline=a||new Yd(this._driver,t,0),o.push(this.currentTimeline)}get params(){return this.options.params}updateOptions(e,t){if(!e)return;const i=e;let n=this.options;null!=i.duration&&(n.duration=hd(i.duration)),null!=i.delay&&(n.delay=hd(i.delay));const r=i.params;if(r){let e=n.params;e||(e=this.options.params={}),Object.keys(r).forEach(i=>{t&&e.hasOwnProperty(i)||(e[i]=wd(r[i],e,this.errors))})}}_copyOptions(){const e={};if(this.options){const t=this.options.params;if(t){const i=e.params={};Object.keys(t).forEach(e=>{i[e]=t[e]})}}return e}createSubContext(e=null,t,i){const n=t||this.element,r=new Zd(this._driver,n,this.subInstructions,this._enterClassName,this._leaveClassName,this.errors,this.timelines,this.currentTimeline.fork(n,i||0));return r.previousNode=this.previousNode,r.currentAnimateTimings=this.currentAnimateTimings,r.options=this._copyOptions(),r.updateOptions(e),r.currentQueryIndex=this.currentQueryIndex,r.currentQueryTotal=this.currentQueryTotal,r.parentContext=this,this.subContextCount++,r}transformIntoNewTimeline(e){return this.previousNode=Gd,this.currentTimeline=this.currentTimeline.fork(this.element,e),this.timelines.push(this.currentTimeline),this.currentTimeline}appendInstructionToTimeline(e,t,i){const n={duration:null!=t?t:e.duration,delay:this.currentTimeline.currentTime+(null!=i?i:0)+e.delay,easing:""},r=new Xd(this._driver,e.element,e.keyframes,e.preStyleProps,e.postStyleProps,n,e.stretchStartingKeyframe);return this.timelines.push(r),n}incrementTime(e){this.currentTimeline.forwardTime(this.currentTimeline.duration+e)}delayNextStep(e){e>0&&this.currentTimeline.delayNextStep(e)}invokeQuery(e,t,i,n,r,s){let o=[];if(n&&o.push(this.element),e.length>0){e=(e=e.replace(zd,"."+this._enterClassName)).replace(Wd,"."+this._leaveClassName);let t=this._driver.query(this.element,e,1!=i);0!==i&&(t=i<0?t.slice(t.length+i,t.length):t.slice(0,i)),o.push(...t)}return r||0!=o.length||s.push(`\`query("${t}")\` returned zero elements. (Use \`query("${t}", { optional: true })\` if you wish to allow this.)`),o}}class Yd{constructor(e,t,i,n){this._driver=e,this.element=t,this.startTime=i,this._elementTimelineStylesLookup=n,this.duration=0,this._previousKeyframe={},this._currentKeyframe={},this._keyframes=new Map,this._styleSummary={},this._pendingStyles={},this._backFill={},this._currentEmptyStepKeyframe=null,this._elementTimelineStylesLookup||(this._elementTimelineStylesLookup=new Map),this._localTimelineStyles=Object.create(this._backFill,{}),this._globalTimelineStyles=this._elementTimelineStylesLookup.get(t),this._globalTimelineStyles||(this._globalTimelineStyles=this._localTimelineStyles,this._elementTimelineStylesLookup.set(t,this._localTimelineStyles)),this._loadKeyframe()}containsAnimation(){switch(this._keyframes.size){case 0:return!1;case 1:return this.getCurrentStyleProperties().length>0;default:return!0}}getCurrentStyleProperties(){return Object.keys(this._currentKeyframe)}get currentTime(){return this.startTime+this.duration}delayNextStep(e){const t=1==this._keyframes.size&&Object.keys(this._pendingStyles).length;this.duration||t?(this.forwardTime(this.currentTime+e),t&&this.snapshotCurrentStyles()):this.startTime+=e}fork(e,t){return this.applyStylesToKeyframe(),new Yd(this._driver,e,t||this.currentTime,this._elementTimelineStylesLookup)}_loadKeyframe(){this._currentKeyframe&&(this._previousKeyframe=this._currentKeyframe),this._currentKeyframe=this._keyframes.get(this.duration),this._currentKeyframe||(this._currentKeyframe=Object.create(this._backFill,{}),this._keyframes.set(this.duration,this._currentKeyframe))}forwardFrame(){this.duration+=1,this._loadKeyframe()}forwardTime(e){this.applyStylesToKeyframe(),this.duration=e,this._loadKeyframe()}_updateStyle(e,t){this._localTimelineStyles[e]=t,this._globalTimelineStyles[e]=t,this._styleSummary[e]={time:this.currentTime,value:t}}allowOnlyTimelineStyles(){return this._currentEmptyStepKeyframe!==this._currentKeyframe}applyEmptyStep(e){e&&(this._previousKeyframe.easing=e),Object.keys(this._globalTimelineStyles).forEach(e=>{this._backFill[e]=this._globalTimelineStyles[e]||xu,this._currentKeyframe[e]=xu}),this._currentEmptyStepKeyframe=this._currentKeyframe}setStyles(e,t,i,n){t&&(this._previousKeyframe.easing=t);const r=n&&n.params||{},s=function(e,t){const i={};let n;return e.forEach(e=>{"*"===e?(n=n||Object.keys(t),n.forEach(e=>{i[e]=xu})):pd(e,!1,i)}),i}(e,this._globalTimelineStyles);Object.keys(s).forEach(e=>{const t=wd(s[e],r,i);this._pendingStyles[e]=t,this._localTimelineStyles.hasOwnProperty(e)||(this._backFill[e]=this._globalTimelineStyles.hasOwnProperty(e)?this._globalTimelineStyles[e]:xu),this._updateStyle(e,t)})}applyStylesToKeyframe(){const e=this._pendingStyles,t=Object.keys(e);0!=t.length&&(this._pendingStyles={},t.forEach(t=>{this._currentKeyframe[t]=e[t]}),Object.keys(this._localTimelineStyles).forEach(e=>{this._currentKeyframe.hasOwnProperty(e)||(this._currentKeyframe[e]=this._localTimelineStyles[e])}))}snapshotCurrentStyles(){Object.keys(this._localTimelineStyles).forEach(e=>{const t=this._localTimelineStyles[e];this._pendingStyles[e]=t,this._updateStyle(e,t)})}getFinalKeyframe(){return this._keyframes.get(this.duration)}get properties(){const e=[];for(let t in this._currentKeyframe)e.push(t);return e}mergeTimelineCollectedStyles(e){Object.keys(e._styleSummary).forEach(t=>{const i=this._styleSummary[t],n=e._styleSummary[t];(!i||n.time>i.time)&&this._updateStyle(t,n.value)})}buildKeyframes(){this.applyStylesToKeyframe();const e=new Set,t=new Set,i=1===this._keyframes.size&&0===this.duration;let n=[];this._keyframes.forEach((r,s)=>{const o=pd(r,!0);Object.keys(o).forEach(i=>{const n=o[i];"!"==n?e.add(i):n==xu&&t.add(i)}),i||(o.offset=s/this.duration),n.push(o)});const r=e.size?Sd(e.values()):[],s=t.size?Sd(t.values()):[];if(i){const e=n[0],t=fd(e);e.offset=0,t.offset=1,n=[e,t]}return Ud(this.element,n,r,s,this.duration,this.startTime,this.easing,!1)}}class Xd extends Yd{constructor(e,t,i,n,r,s,o=!1){super(e,t,s.delay),this.element=t,this.keyframes=i,this.preStyleProps=n,this.postStyleProps=r,this._stretchStartingKeyframe=o,this.timings={duration:s.duration,delay:s.delay,easing:s.easing}}containsAnimation(){return this.keyframes.length>1}buildKeyframes(){let e=this.keyframes,{delay:t,duration:i,easing:n}=this.timings;if(this._stretchStartingKeyframe&&t){const r=[],s=i+t,o=t/s,a=pd(e[0],!1);a.offset=0,r.push(a);const l=pd(e[0],!1);l.offset=Qd(o),r.push(l);const c=e.length-1;for(let n=1;n<=c;n++){let o=pd(e[n],!1);o.offset=Qd((t+o.offset*i)/s),r.push(o)}i=s,t=0,n="",e=r}return Ud(this.element,e,this.preStyleProps,this.postStyleProps,i,t,n,!0)}}function Qd(e,t=3){const i=Math.pow(10,t-1);return Math.round(e*i)/i}class Jd{}class ef extends Jd{normalizePropertyName(e,t){return xd(e)}normalizeStyleValue(e,t,i,n){let r="";const s=i.toString().trim();if(tf[t]&&0!==i&&"0"!==i)if("number"==typeof i)r="px";else{const t=i.match(/^[+-]?[\d\.]+([a-z]*)$/);t&&0==t[1].length&&n.push(`Please provide a CSS unit value for ${e}:${i}`)}return s+r}}const tf=(()=>function(e){const t={};return e.forEach(e=>t[e]=!0),t}("width,height,minWidth,minHeight,maxWidth,maxHeight,left,top,bottom,right,fontSize,outlineWidth,outlineOffset,paddingTop,paddingLeft,paddingBottom,paddingRight,marginTop,marginLeft,marginBottom,marginRight,borderRadius,borderWidth,borderTopWidth,borderLeftWidth,borderRightWidth,borderBottomWidth,textIndent,perspective".split(",")))();function nf(e,t,i,n,r,s,o,a,l,c,h,u,d){return{type:0,element:e,triggerName:t,isRemovalTransition:r,fromState:i,fromStyles:s,toState:n,toStyles:o,timelines:a,queriedElements:l,preStyleProps:c,postStyleProps:h,totalTime:u,errors:d}}const rf={};class sf{constructor(e,t,i){this._triggerName=e,this.ast=t,this._stateStyles=i}match(e,t,i,n){return function(e,t,i,n,r){return e.some(e=>e(t,i,n,r))}(this.ast.matchers,e,t,i,n)}buildStyles(e,t,i){const n=this._stateStyles["*"],r=this._stateStyles[e],s=n?n.buildStyles(t,i):{};return r?r.buildStyles(t,i):s}build(e,t,i,n,r,s,o,a,l,c){const h=[],u=this.ast.options&&this.ast.options.params||rf,d=this.buildStyles(i,o&&o.params||rf,h),f=a&&a.params||rf,p=this.buildStyles(n,f,h),_=new Set,m=new Map,g=new Map,v="void"===n,y={params:Object.assign(Object.assign({},u),f)},b=c?[]:$d(e,t,this.ast.animation,r,s,d,p,y,l,h);let C=0;if(b.forEach(e=>{C=Math.max(e.duration+e.delay,C)}),h.length)return nf(t,this._triggerName,i,n,v,d,p,[],[],m,g,C,h);b.forEach(e=>{const i=e.element,n=qu(m,i,{});e.preStyleProps.forEach(e=>n[e]=!0);const r=qu(g,i,{});e.postStyleProps.forEach(e=>r[e]=!0),i!==t&&_.add(i)});const w=Sd(_.values());return nf(t,this._triggerName,i,n,v,d,p,b,w,m,g,C)}}class of{constructor(e,t){this.styles=e,this.defaultParams=t}buildStyles(e,t){const i={},n=fd(this.defaultParams);return Object.keys(e).forEach(t=>{const i=e[t];null!=i&&(n[t]=i)}),this.styles.styles.forEach(e=>{if("string"!=typeof e){const r=e;Object.keys(r).forEach(e=>{let s=r[e];s.length>1&&(s=wd(s,n,t)),i[e]=s})}}),i}}class af{constructor(e,t){this.name=e,this.ast=t,this.transitionFactories=[],this.states={},t.states.forEach(e=>{this.states[e.name]=new of(e.style,e.options&&e.options.params||{})}),lf(this.states,"true","1"),lf(this.states,"false","0"),t.transitions.forEach(t=>{this.transitionFactories.push(new sf(e,t,this.states))}),this.fallbackTransition=new sf(e,{type:1,animation:{type:2,steps:[],options:null},matchers:[(e,t)=>!0],options:null,queryCount:0,depCount:0},this.states)}get containsQueries(){return this.ast.queryCount>0}matchTransition(e,t,i,n){return this.transitionFactories.find(r=>r.match(e,t,i,n))||null}matchStyles(e,t,i){return this.fallbackTransition.buildStyles(e,t,i)}}function lf(e,t,i){e.hasOwnProperty(t)?e.hasOwnProperty(i)||(e[i]=e[t]):e.hasOwnProperty(i)&&(e[t]=e[i])}const cf=new qd;class hf{constructor(e,t,i){this.bodyNode=e,this._driver=t,this._normalizer=i,this._animations={},this._playersById={},this.players=[]}register(e,t){const i=[],n=Fd(this._driver,t,i);if(i.length)throw new Error("Unable to build the animation due to the following errors: "+i.join("\n"));this._animations[e]=n}_buildPlayer(e,t,i){const n=e.element,r=ju(0,this._normalizer,0,e.keyframes,t,i);return this._driver.animate(n,r,e.duration,e.delay,e.easing,[],!0)}create(e,t,i={}){const n=[],r=this._animations[e];let s;const o=new Map;if(r?(s=$d(this._driver,t,r,rd,sd,{},{},i,cf,n),s.forEach(e=>{const t=qu(o,e.element,{});e.postStyleProps.forEach(e=>t[e]=null)})):(n.push("The requested animation doesn't exist or has already been destroyed"),s=[]),n.length)throw new Error("Unable to create the animation due to the following errors: "+n.join("\n"));o.forEach((e,t)=>{Object.keys(e).forEach(i=>{e[i]=this._driver.computeStyle(t,i,xu)})});const a=Bu(s.map(e=>{const t=o.get(e.element);return this._buildPlayer(e,{},t)}));return this._playersById[e]=a,a.onDestroy(()=>this.destroy(e)),this.players.push(a),a}destroy(e){const t=this._getPlayer(e);t.destroy(),delete this._playersById[e];const i=this.players.indexOf(t);i>=0&&this.players.splice(i,1)}_getPlayer(e){const t=this._playersById[e];if(!t)throw new Error("Unable to find the timeline player referenced by "+e);return t}listen(e,t,i,n){const r=Uu(t,"","","");return Nu(this._getPlayer(e),i,r,n),()=>{}}command(e,t,i,n){if("register"==i)return void this.register(e,n[0]);if("create"==i)return void this.create(e,t,n[0]||{});const r=this._getPlayer(e);switch(i){case"play":r.play();break;case"pause":r.pause();break;case"reset":r.reset();break;case"restart":r.restart();break;case"finish":r.finish();break;case"init":r.init();break;case"setPosition":r.setPosition(parseFloat(n[0]));break;case"destroy":this.destroy(e)}}}const uf="ng-animate-queued",df="ng-animate-disabled",ff=".ng-animate-disabled",pf=[],_f={namespaceId:"",setForRemoval:!1,setForMove:!1,hasAnimation:!1,removedBeforeQueried:!1},mf={namespaceId:"",setForMove:!1,setForRemoval:!1,hasAnimation:!1,removedBeforeQueried:!0};class gf{constructor(e,t=""){this.namespaceId=t;const i=e&&e.hasOwnProperty("value");if(this.value=null!=(n=i?e.value:e)?n:null,i){const t=fd(e);delete t.value,this.options=t}else this.options={};var n;this.options.params||(this.options.params={})}get params(){return this.options.params}absorbOptions(e){const t=e.params;if(t){const e=this.options.params;Object.keys(t).forEach(i=>{null==e[i]&&(e[i]=t[i])})}}}const vf="void",yf=new gf(vf);class bf{constructor(e,t,i){this.id=e,this.hostElement=t,this._engine=i,this.players=[],this._triggers={},this._queue=[],this._elementListeners=new Map,this._hostClassName="ng-tns-"+e,Af(t,this._hostClassName)}listen(e,t,i,n){if(!this._triggers.hasOwnProperty(t))throw new Error(`Unable to listen on the animation trigger event "${i}" because the animation trigger "${t}" doesn't exist!`);if(null==i||0==i.length)throw new Error(`Unable to listen on the animation trigger "${t}" because the provided event is undefined!`);if("start"!=(r=i)&&"done"!=r)throw new Error(`The provided animation trigger event "${i}" for the animation trigger "${t}" is not supported!`);var r;const s=qu(this._elementListeners,e,[]),o={name:t,phase:i,callback:n};s.push(o);const a=qu(this._engine.statesByElement,e,{});return a.hasOwnProperty(t)||(Af(e,od),Af(e,"ng-trigger-"+t),a[t]=yf),()=>{this._engine.afterFlush(()=>{const e=s.indexOf(o);e>=0&&s.splice(e,1),this._triggers[t]||delete a[t]})}}register(e,t){return!this._triggers[e]&&(this._triggers[e]=t,!0)}_getTrigger(e){const t=this._triggers[e];if(!t)throw new Error(`The provided animation trigger "${e}" has not been registered!`);return t}trigger(e,t,i,n=!0){const r=this._getTrigger(t),s=new wf(this.id,t,e);let o=this._engine.statesByElement.get(e);o||(Af(e,od),Af(e,"ng-trigger-"+t),this._engine.statesByElement.set(e,o={}));let a=o[t];const l=new gf(i,this.id);if(!(i&&i.hasOwnProperty("value"))&&a&&l.absorbOptions(a.options),o[t]=l,a||(a=yf),l.value!==vf&&a.value===l.value){if(!function(e,t){const i=Object.keys(e),n=Object.keys(t);if(i.length!=n.length)return!1;for(let r=0;r{vd(e,i),gd(e,n)})}return}const c=qu(this._engine.playersByElement,e,[]);c.forEach(e=>{e.namespaceId==this.id&&e.triggerName==t&&e.queued&&e.destroy()});let h=r.matchTransition(a.value,l.value,e,l.params),u=!1;if(!h){if(!n)return;h=r.fallbackTransition,u=!0}return this._engine.totalQueuedPlayers++,this._queue.push({element:e,triggerName:t,transition:h,fromState:a,toState:l,player:s,isFallbackTransition:u}),u||(Af(e,uf),s.onStart(()=>{Tf(e,uf)})),s.onDone(()=>{let t=this.players.indexOf(s);t>=0&&this.players.splice(t,1);const i=this._engine.playersByElement.get(e);if(i){let e=i.indexOf(s);e>=0&&i.splice(e,1)}}),this.players.push(s),c.push(s),s}deregister(e){delete this._triggers[e],this._engine.statesByElement.forEach((t,i)=>{delete t[e]}),this._elementListeners.forEach((t,i)=>{this._elementListeners.set(i,t.filter(t=>t.name!=e))})}clearElementCache(e){this._engine.statesByElement.delete(e),this._elementListeners.delete(e);const t=this._engine.playersByElement.get(e);t&&(t.forEach(e=>e.destroy()),this._engine.playersByElement.delete(e))}_signalRemovalForInnerTriggers(e,t){const i=this._engine.driver.query(e,ad,!0);i.forEach(e=>{if(e.__ng_removed)return;const i=this._engine.fetchNamespacesByElement(e);i.size?i.forEach(i=>i.triggerLeaveAnimation(e,t,!1,!0)):this.clearElementCache(e)}),this._engine.afterFlushAnimationsDone(()=>i.forEach(e=>this.clearElementCache(e)))}triggerLeaveAnimation(e,t,i,n){const r=this._engine.statesByElement.get(e);if(r){const s=[];if(Object.keys(r).forEach(t=>{if(this._triggers[t]){const i=this.trigger(e,t,vf,n);i&&s.push(i)}}),s.length)return this._engine.markElementAsRemoved(this.id,e,!0,t),i&&Bu(s).onDone(()=>this._engine.processLeaveNode(e)),!0}return!1}prepareLeaveAnimationListeners(e){const t=this._elementListeners.get(e),i=this._engine.statesByElement.get(e);if(t&&i){const n=new Set;t.forEach(t=>{const r=t.name;if(n.has(r))return;n.add(r);const s=this._triggers[r].fallbackTransition,o=i[r]||yf,a=new gf(vf),l=new wf(this.id,r,e);this._engine.totalQueuedPlayers++,this._queue.push({element:e,triggerName:r,transition:s,fromState:o,toState:a,player:l,isFallbackTransition:!0})})}}removeNode(e,t){const i=this._engine;if(e.childElementCount&&this._signalRemovalForInnerTriggers(e,t),this.triggerLeaveAnimation(e,t,!0))return;let n=!1;if(i.totalAnimations){const t=i.players.length?i.playersByQueriedElement.get(e):[];if(t&&t.length)n=!0;else{let t=e;for(;t=t.parentNode;)if(i.statesByElement.get(t)){n=!0;break}}}if(this.prepareLeaveAnimationListeners(e),n)i.markElementAsRemoved(this.id,e,!1,t);else{const n=e.__ng_removed;n&&n!==_f||(i.afterFlush(()=>this.clearElementCache(e)),i.destroyInnerAnimations(e),i._onRemovalComplete(e,t))}}insertNode(e,t){Af(e,this._hostClassName)}drainQueuedTransitions(e){const t=[];return this._queue.forEach(i=>{const n=i.player;if(n.destroyed)return;const r=i.element,s=this._elementListeners.get(r);s&&s.forEach(t=>{if(t.name==i.triggerName){const n=Uu(r,i.triggerName,i.fromState.value,i.toState.value);n._data=e,Nu(i.player,t.phase,n,t.callback)}}),n.markedForDestroy?this._engine.afterFlush(()=>{n.destroy()}):t.push(i)}),this._queue=[],t.sort((e,t)=>{const i=e.transition.ast.depCount,n=t.transition.ast.depCount;return 0==i||0==n?i-n:this._engine.driver.containsElement(e.element,t.element)?1:-1})}destroy(e){this.players.forEach(e=>e.destroy()),this._signalRemovalForInnerTriggers(this.hostElement,e)}elementContainsData(e){let t=!1;return this._elementListeners.has(e)&&(t=!0),t=!!this._queue.find(t=>t.element===e)||t,t}}class Cf{constructor(e,t,i){this.bodyNode=e,this.driver=t,this._normalizer=i,this.players=[],this.newHostElements=new Map,this.playersByElement=new Map,this.playersByQueriedElement=new Map,this.statesByElement=new Map,this.disabledNodes=new Set,this.totalAnimations=0,this.totalQueuedPlayers=0,this._namespaceLookup={},this._namespaceList=[],this._flushFns=[],this._whenQuietFns=[],this.namespacesByHostElement=new Map,this.collectedEnterElements=[],this.collectedLeaveElements=[],this.onRemovalComplete=(e,t)=>{}}_onRemovalComplete(e,t){this.onRemovalComplete(e,t)}get queuedPlayers(){const e=[];return this._namespaceList.forEach(t=>{t.players.forEach(t=>{t.queued&&e.push(t)})}),e}createNamespace(e,t){const i=new bf(e,t,this);return t.parentNode?this._balanceNamespaceList(i,t):(this.newHostElements.set(t,i),this.collectEnterElement(t)),this._namespaceLookup[e]=i}_balanceNamespaceList(e,t){const i=this._namespaceList.length-1;if(i>=0){let n=!1;for(let r=i;r>=0;r--)if(this.driver.containsElement(this._namespaceList[r].hostElement,t)){this._namespaceList.splice(r+1,0,e),n=!0;break}n||this._namespaceList.splice(0,0,e)}else this._namespaceList.push(e);return this.namespacesByHostElement.set(t,e),e}register(e,t){let i=this._namespaceLookup[e];return i||(i=this.createNamespace(e,t)),i}registerTrigger(e,t,i){let n=this._namespaceLookup[e];n&&n.register(t,i)&&this.totalAnimations++}destroy(e,t){if(!e)return;const i=this._fetchNamespace(e);this.afterFlush(()=>{this.namespacesByHostElement.delete(i.hostElement),delete this._namespaceLookup[e];const t=this._namespaceList.indexOf(i);t>=0&&this._namespaceList.splice(t,1)}),this.afterFlushAnimationsDone(()=>i.destroy(t))}_fetchNamespace(e){return this._namespaceLookup[e]}fetchNamespacesByElement(e){const t=new Set,i=this.statesByElement.get(e);if(i){const e=Object.keys(i);for(let n=0;n=0&&this.collectedLeaveElements.splice(e,1)}if(e){const n=this._fetchNamespace(e);n&&n.insertNode(t,i)}n&&this.collectEnterElement(t)}collectEnterElement(e){this.collectedEnterElements.push(e)}markElementAsDisabled(e,t){t?this.disabledNodes.has(e)||(this.disabledNodes.add(e),Af(e,df)):this.disabledNodes.has(e)&&(this.disabledNodes.delete(e),Tf(e,df))}removeNode(e,t,i,n){if(Sf(t)){const r=e?this._fetchNamespace(e):null;if(r?r.removeNode(t,n):this.markElementAsRemoved(e,t,!1,n),i){const i=this.namespacesByHostElement.get(t);i&&i.id!==e&&i.removeNode(t,n)}}else this._onRemovalComplete(t,n)}markElementAsRemoved(e,t,i,n){this.collectedLeaveElements.push(t),t.__ng_removed={namespaceId:e,setForRemoval:n,hasAnimation:i,removedBeforeQueried:!1}}listen(e,t,i,n,r){return Sf(t)?this._fetchNamespace(e).listen(t,i,n,r):()=>{}}_buildInstruction(e,t,i,n,r){return e.transition.build(this.driver,e.element,e.fromState.value,e.toState.value,i,n,e.fromState.options,e.toState.options,t,r)}destroyInnerAnimations(e){let t=this.driver.query(e,ad,!0);t.forEach(e=>this.destroyActiveAnimationsForElement(e)),0!=this.playersByQueriedElement.size&&(t=this.driver.query(e,cd,!0),t.forEach(e=>this.finishActiveQueriedAnimationOnElement(e)))}destroyActiveAnimationsForElement(e){const t=this.playersByElement.get(e);t&&t.forEach(e=>{e.queued?e.markedForDestroy=!0:e.destroy()})}finishActiveQueriedAnimationOnElement(e){const t=this.playersByQueriedElement.get(e);t&&t.forEach(e=>e.finish())}whenRenderingDone(){return new Promise(e=>{if(this.players.length)return Bu(this.players).onDone(()=>e());e()})}processLeaveNode(e){const t=e.__ng_removed;if(t&&t.setForRemoval){if(e.__ng_removed=_f,t.namespaceId){this.destroyInnerAnimations(e);const i=this._fetchNamespace(t.namespaceId);i&&i.clearElementCache(e)}this._onRemovalComplete(e,t.setForRemoval)}this.driver.matchesElement(e,ff)&&this.markElementAsDisabled(e,!1),this.driver.query(e,ff,!0).forEach(e=>{this.markElementAsDisabled(e,!1)})}flush(e=-1){let t=[];if(this.newHostElements.size&&(this.newHostElements.forEach((e,t)=>this._balanceNamespaceList(e,t)),this.newHostElements.clear()),this.totalAnimations&&this.collectedEnterElements.length)for(let i=0;ie()),this._flushFns=[],this._whenQuietFns.length){const e=this._whenQuietFns;this._whenQuietFns=[],t.length?Bu(t).onDone(()=>{e.forEach(e=>e())}):e.forEach(e=>e())}}reportError(e){throw new Error("Unable to process animations due to the following failed trigger transitions\n "+e.join("\n"))}_flushAnimations(e,t){const i=new qd,n=[],r=new Map,s=[],o=new Map,a=new Map,l=new Map,c=new Set;this.disabledNodes.forEach(e=>{c.add(e);const t=this.driver.query(e,".ng-animate-queued",!0);for(let i=0;i{const i=rd+p++;f.set(t,i),e.forEach(e=>Af(e,i))});const _=[],m=new Set,g=new Set;for(let R=0;Rm.add(e)):g.add(e))}const v=new Map,y=Ef(u,Array.from(m));y.forEach((e,t)=>{const i=sd+p++;v.set(t,i),e.forEach(e=>Af(e,i))}),e.push(()=>{d.forEach((e,t)=>{const i=f.get(t);e.forEach(e=>Tf(e,i))}),y.forEach((e,t)=>{const i=v.get(t);e.forEach(e=>Tf(e,i))}),_.forEach(e=>{this.processLeaveNode(e)})});const b=[],C=[];for(let R=this._namespaceList.length-1;R>=0;R--)this._namespaceList[R].drainQueuedTransitions(t).forEach(e=>{const t=e.player,r=e.element;if(b.push(t),this.collectedEnterElements.length){const e=r.__ng_removed;if(e&&e.setForMove)return void t.destroy()}const c=!h||!this.driver.containsElement(h,r),u=v.get(r),d=f.get(r),p=this._buildInstruction(e,i,d,u,c);if(p.errors&&p.errors.length)C.push(p);else{if(c)return t.onStart(()=>vd(r,p.fromStyles)),t.onDestroy(()=>gd(r,p.toStyles)),void n.push(t);if(e.isFallbackTransition)return t.onStart(()=>vd(r,p.fromStyles)),t.onDestroy(()=>gd(r,p.toStyles)),void n.push(t);p.timelines.forEach(e=>e.stretchStartingKeyframe=!0),i.append(r,p.timelines),s.push({instruction:p,player:t,element:r}),p.queriedElements.forEach(e=>qu(o,e,[]).push(t)),p.preStyleProps.forEach((e,t)=>{const i=Object.keys(e);if(i.length){let e=a.get(t);e||a.set(t,e=new Set),i.forEach(t=>e.add(t))}}),p.postStyleProps.forEach((e,t)=>{const i=Object.keys(e);let n=l.get(t);n||l.set(t,n=new Set),i.forEach(e=>n.add(e))})}});if(C.length){const e=[];C.forEach(t=>{e.push(`@${t.triggerName} has failed due to:\n`),t.errors.forEach(t=>e.push(`- ${t}\n`))}),b.forEach(e=>e.destroy()),this.reportError(e)}const w=new Map,S=new Map;s.forEach(e=>{const t=e.element;i.has(t)&&(S.set(t,t),this._beforeAnimationBuild(e.player.namespaceId,e.instruction,w))}),n.forEach(e=>{const t=e.element;this._getPreviousPlayers(t,!1,e.namespaceId,e.triggerName,null).forEach(e=>{qu(w,t,[]).push(e),e.destroy()})});const k=_.filter(e=>Rf(e,a,l)),x=new Map;xf(x,this.driver,g,l,xu).forEach(e=>{Rf(e,a,l)&&k.push(e)});const E=new Map;d.forEach((e,t)=>{xf(E,this.driver,new Set(e),a,"!")}),k.forEach(e=>{const t=x.get(e),i=E.get(e);x.set(e,Object.assign(Object.assign({},t),i))});const A=[],T=[],O={};s.forEach(e=>{const{element:t,player:s,instruction:o}=e;if(i.has(t)){if(c.has(t))return s.onDestroy(()=>gd(t,o.toStyles)),s.disabled=!0,s.overrideTotalTime(o.totalTime),void n.push(s);let e=O;if(S.size>1){let i=t;const n=[];for(;i=i.parentNode;){const t=S.get(i);if(t){e=t;break}n.push(i)}n.forEach(t=>S.set(t,e))}const i=this._buildAnimation(s.namespaceId,o,w,r,E,x);if(s.setRealPlayer(i),e===O)A.push(s);else{const t=this.playersByElement.get(e);t&&t.length&&(s.parentPlayer=Bu(t)),n.push(s)}}else vd(t,o.fromStyles),s.onDestroy(()=>gd(t,o.toStyles)),T.push(s),c.has(t)&&n.push(s)}),T.forEach(e=>{const t=r.get(e.element);if(t&&t.length){const i=Bu(t);e.setRealPlayer(i)}}),n.forEach(e=>{e.parentPlayer?e.syncPlayerEvents(e.parentPlayer):e.destroy()});for(let R=0;R<_.length;R++){const e=_[R],t=e.__ng_removed;if(Tf(e,sd),t&&t.hasAnimation)continue;let i=[];if(o.size){let t=o.get(e);t&&t.length&&i.push(...t);let n=this.driver.query(e,cd,!0);for(let e=0;e!e.destroyed);n.length?Of(this,e,n):this.processLeaveNode(e)}return _.length=0,A.forEach(e=>{this.players.push(e),e.onDone(()=>{e.destroy();const t=this.players.indexOf(e);this.players.splice(t,1)}),e.play()}),A}elementContainsData(e,t){let i=!1;const n=t.__ng_removed;return n&&n.setForRemoval&&(i=!0),this.playersByElement.has(t)&&(i=!0),this.playersByQueriedElement.has(t)&&(i=!0),this.statesByElement.has(t)&&(i=!0),this._fetchNamespace(e).elementContainsData(t)||i}afterFlush(e){this._flushFns.push(e)}afterFlushAnimationsDone(e){this._whenQuietFns.push(e)}_getPreviousPlayers(e,t,i,n,r){let s=[];if(t){const t=this.playersByQueriedElement.get(e);t&&(s=t)}else{const t=this.playersByElement.get(e);if(t){const e=!r||r==vf;t.forEach(t=>{t.queued||(e||t.triggerName==n)&&s.push(t)})}}return(i||n)&&(s=s.filter(e=>!(i&&i!=e.namespaceId||n&&n!=e.triggerName))),s}_beforeAnimationBuild(e,t,i){const n=t.element,r=t.isRemovalTransition?void 0:e,s=t.isRemovalTransition?void 0:t.triggerName;for(const o of t.timelines){const e=o.element,a=e!==n,l=qu(i,e,[]);this._getPreviousPlayers(e,a,r,s,t.toState).forEach(e=>{const t=e.getRealPlayer();t.beforeDestroy&&t.beforeDestroy(),e.destroy(),l.push(e)})}vd(n,t.fromStyles)}_buildAnimation(e,t,i,n,r,s){const o=t.triggerName,a=t.element,l=[],c=new Set,h=new Set,u=t.timelines.map(t=>{const u=t.element;c.add(u);const d=u.__ng_removed;if(d&&d.removedBeforeQueried)return new Mu(t.duration,t.delay);const f=u!==a,p=function(e){const t=[];return function e(t,i){for(let n=0;ne.getRealPlayer())).filter(e=>!!e.element&&e.element===u),_=r.get(u),m=s.get(u),g=ju(0,this._normalizer,0,t.keyframes,_,m),v=this._buildPlayer(t,g,p);if(t.subTimeline&&n&&h.add(u),f){const t=new wf(e,o,u);t.setRealPlayer(v),l.push(t)}return v});l.forEach(e=>{qu(this.playersByQueriedElement,e.element,[]).push(e),e.onDone(()=>function(e,t,i){let n;if(e instanceof Map){if(n=e.get(t),n){if(n.length){const e=n.indexOf(i);n.splice(e,1)}0==n.length&&e.delete(t)}}else if(n=e[t],n){if(n.length){const e=n.indexOf(i);n.splice(e,1)}0==n.length&&delete e[t]}return n}(this.playersByQueriedElement,e.element,e))}),c.forEach(e=>Af(e,ld));const d=Bu(u);return d.onDestroy(()=>{c.forEach(e=>Tf(e,ld)),gd(a,t.toStyles)}),h.forEach(e=>{qu(n,e,[]).push(d)}),d}_buildPlayer(e,t,i){return t.length>0?this.driver.animate(e.element,t,e.duration,e.delay,e.easing,i):new Mu(e.duration,e.delay)}}class wf{constructor(e,t,i){this.namespaceId=e,this.triggerName=t,this.element=i,this._player=new Mu,this._containsRealPlayer=!1,this._queuedCallbacks={},this.destroyed=!1,this.markedForDestroy=!1,this.disabled=!1,this.queued=!0,this.totalTime=0}setRealPlayer(e){this._containsRealPlayer||(this._player=e,Object.keys(this._queuedCallbacks).forEach(t=>{this._queuedCallbacks[t].forEach(i=>Nu(e,t,void 0,i))}),this._queuedCallbacks={},this._containsRealPlayer=!0,this.overrideTotalTime(e.totalTime),this.queued=!1)}getRealPlayer(){return this._player}overrideTotalTime(e){this.totalTime=e}syncPlayerEvents(e){const t=this._player;t.triggerCallback&&e.onStart(()=>t.triggerCallback("start")),e.onDone(()=>this.finish()),e.onDestroy(()=>this.destroy())}_queueEvent(e,t){qu(this._queuedCallbacks,e,[]).push(t)}onDone(e){this.queued&&this._queueEvent("done",e),this._player.onDone(e)}onStart(e){this.queued&&this._queueEvent("start",e),this._player.onStart(e)}onDestroy(e){this.queued&&this._queueEvent("destroy",e),this._player.onDestroy(e)}init(){this._player.init()}hasStarted(){return!this.queued&&this._player.hasStarted()}play(){!this.queued&&this._player.play()}pause(){!this.queued&&this._player.pause()}restart(){!this.queued&&this._player.restart()}finish(){this._player.finish()}destroy(){this.destroyed=!0,this._player.destroy()}reset(){!this.queued&&this._player.reset()}setPosition(e){this.queued||this._player.setPosition(e)}getPosition(){return this.queued?0:this._player.getPosition()}triggerCallback(e){const t=this._player;t.triggerCallback&&t.triggerCallback(e)}}function Sf(e){return e&&1===e.nodeType}function kf(e,t){const i=e.style.display;return e.style.display=null!=t?t:"none",i}function xf(e,t,i,n,r){const s=[];i.forEach(e=>s.push(kf(e)));const o=[];n.forEach((i,n)=>{const s={};i.forEach(e=>{const i=s[e]=t.computeStyle(n,e,r);i&&0!=i.length||(n.__ng_removed=mf,o.push(n))}),e.set(n,s)});let a=0;return i.forEach(e=>kf(e,s[a++])),o}function Ef(e,t){const i=new Map;if(e.forEach(e=>i.set(e,[])),0==t.length)return i;const n=new Set(t),r=new Map;return t.forEach(e=>{const t=function e(t){if(!t)return 1;let s=r.get(t);if(s)return s;const o=t.parentNode;return s=i.has(o)?o:n.has(o)?1:e(o),r.set(t,s),s}(e);1!==t&&i.get(t).push(e)}),i}function Af(e,t){if(e.classList)e.classList.add(t);else{let i=e.$$classes;i||(i=e.$$classes={}),i[t]=!0}}function Tf(e,t){if(e.classList)e.classList.remove(t);else{let i=e.$$classes;i&&delete i[t]}}function Of(e,t,i){Bu(i).onDone(()=>e.processLeaveNode(t))}function Rf(e,t,i){const n=i.get(e);if(!n)return!1;let r=t.get(e);return r?n.forEach(e=>r.add(e)):t.set(e,n),i.delete(e),!0}class Lf{constructor(e,t,i){this.bodyNode=e,this._driver=t,this._triggerCache={},this.onRemovalComplete=(e,t)=>{},this._transitionEngine=new Cf(e,t,i),this._timelineEngine=new hf(e,t,i),this._transitionEngine.onRemovalComplete=(e,t)=>this.onRemovalComplete(e,t)}registerTrigger(e,t,i,n,r){const s=e+"-"+n;let o=this._triggerCache[s];if(!o){const e=[],t=Fd(this._driver,r,e);if(e.length)throw new Error(`The animation trigger "${n}" has failed to build due to the following errors:\n - ${e.join("\n - ")}`);o=function(e,t){return new af(e,t)}(n,t),this._triggerCache[s]=o}this._transitionEngine.registerTrigger(t,n,o)}register(e,t){this._transitionEngine.register(e,t)}destroy(e,t){this._transitionEngine.destroy(e,t)}onInsert(e,t,i,n){this._transitionEngine.insertNode(e,t,i,n)}onRemove(e,t,i,n){this._transitionEngine.removeNode(e,t,n||!1,i)}disableAnimations(e,t){this._transitionEngine.markElementAsDisabled(e,t)}process(e,t,i,n){if("@"==i.charAt(0)){const[e,r]=zu(i);this._timelineEngine.command(e,t,r,n)}else this._transitionEngine.trigger(e,t,i,n)}listen(e,t,i,n,r){if("@"==i.charAt(0)){const[e,n]=zu(i);return this._timelineEngine.listen(e,t,n,r)}return this._transitionEngine.listen(e,t,i,n,r)}flush(e=-1){this._transitionEngine.flush(e)}get players(){return this._transitionEngine.players.concat(this._timelineEngine.players)}whenRenderingDone(){return this._transitionEngine.whenRenderingDone()}}function Df(e,t){let i=null,n=null;return Array.isArray(t)&&t.length?(i=If(t[0]),t.length>1&&(n=If(t[t.length-1]))):t&&(i=If(t)),i||n?new Pf(e,i,n):null}let Pf=(()=>{class e{constructor(t,i,n){this._element=t,this._startStyles=i,this._endStyles=n,this._state=0;let r=e.initialStylesByElement.get(t);r||e.initialStylesByElement.set(t,r={}),this._initialStyles=r}start(){this._state<1&&(this._startStyles&&gd(this._element,this._startStyles,this._initialStyles),this._state=1)}finish(){this.start(),this._state<2&&(gd(this._element,this._initialStyles),this._endStyles&&(gd(this._element,this._endStyles),this._endStyles=null),this._state=1)}destroy(){this.finish(),this._state<3&&(e.initialStylesByElement.delete(this._element),this._startStyles&&(vd(this._element,this._startStyles),this._endStyles=null),this._endStyles&&(vd(this._element,this._endStyles),this._endStyles=null),gd(this._element,this._initialStyles),this._state=3)}}return e.initialStylesByElement=new WeakMap,e})();function If(e){let t=null;const i=Object.keys(e);for(let n=0;nthis._handleCallback(e)}apply(){!function(e,t){const i=zf(e,"").trim();i.length&&(function(e,t){let i=0;for(let n=0;n=this._delay&&i>=this._duration&&this.finish()}finish(){this._finished||(this._finished=!0,this._onDoneFn(),Uf(this._element,this._eventFn,!0))}destroy(){this._destroyed||(this._destroyed=!0,this.finish(),function(e,t){const i=zf(e,"").split(","),n=Vf(i,t);n>=0&&(i.splice(n,1),qf(e,"",i.join(",")))}(this._element,this._name))}}function jf(e,t,i){qf(e,"PlayState",i,Nf(e,t))}function Nf(e,t){const i=zf(e,"");return i.indexOf(",")>0?Vf(i.split(","),t):Vf([i],t)}function Vf(e,t){for(let i=0;i=0)return i;return-1}function Uf(e,t,i){i?e.removeEventListener(Hf,t):e.addEventListener(Hf,t)}function qf(e,t,i,n){const r=Ff+t;if(null!=n){const t=e.style[r];if(t.length){const e=t.split(",");e[n]=i,i=e.join(",")}}e.style[r]=i}function zf(e,t){return e.style[Ff+t]||""}class Wf{constructor(e,t,i,n,r,s,o,a){this.element=e,this.keyframes=t,this.animationName=i,this._duration=n,this._delay=r,this._finalStyles=o,this._specialStyles=a,this._onDoneFns=[],this._onStartFns=[],this._onDestroyFns=[],this._started=!1,this.currentSnapshot={},this._state=0,this.easing=s||"linear",this.totalTime=n+r,this._buildStyler()}onStart(e){this._onStartFns.push(e)}onDone(e){this._onDoneFns.push(e)}onDestroy(e){this._onDestroyFns.push(e)}destroy(){this.init(),this._state>=4||(this._state=4,this._styler.destroy(),this._flushStartFns(),this._flushDoneFns(),this._specialStyles&&this._specialStyles.destroy(),this._onDestroyFns.forEach(e=>e()),this._onDestroyFns=[])}_flushDoneFns(){this._onDoneFns.forEach(e=>e()),this._onDoneFns=[]}_flushStartFns(){this._onStartFns.forEach(e=>e()),this._onStartFns=[]}finish(){this.init(),this._state>=3||(this._state=3,this._styler.finish(),this._flushStartFns(),this._specialStyles&&this._specialStyles.finish(),this._flushDoneFns())}setPosition(e){this._styler.setPosition(e)}getPosition(){return this._styler.getPosition()}hasStarted(){return this._state>=2}init(){this._state>=1||(this._state=1,this._styler.apply(),this._delay&&this._styler.pause())}play(){this.init(),this.hasStarted()||(this._flushStartFns(),this._state=2,this._specialStyles&&this._specialStyles.start()),this._styler.resume()}pause(){this.init(),this._styler.pause()}restart(){this.reset(),this.play()}reset(){this._styler.destroy(),this._buildStyler(),this._styler.apply()}_buildStyler(){this._styler=new Bf(this.element,this.animationName,this._duration,this._delay,this.easing,"forwards",()=>this.finish())}triggerCallback(e){const t="start"==e?this._onStartFns:this._onDoneFns;t.forEach(e=>e()),t.length=0}beforeDestroy(){this.init();const e={};if(this.hasStarted()){const t=this._state>=3;Object.keys(this._finalStyles).forEach(i=>{"offset"!=i&&(e[i]=t?this._finalStyles[i]:Od(this.element,i))})}this.currentSnapshot=e}}class $f extends Mu{constructor(e,t){super(),this.element=e,this._startingStyles={},this.__initialized=!1,this._styles=td(t)}init(){!this.__initialized&&this._startingStyles&&(this.__initialized=!0,Object.keys(this._styles).forEach(e=>{this._startingStyles[e]=this.element.style[e]}),super.init())}play(){this._startingStyles&&(this.init(),Object.keys(this._styles).forEach(e=>this.element.style.setProperty(e,this._styles[e])),super.play())}destroy(){this._startingStyles&&(Object.keys(this._startingStyles).forEach(e=>{const t=this._startingStyles[e];t?this.element.style.setProperty(e,t):this.element.style.removeProperty(e)}),this._startingStyles=null,super.destroy())}}class Kf{constructor(){this._count=0,this._head=document.querySelector("head")}validateStyleProperty(e){return Xu(e)}matchesElement(e,t){return Qu(e,t)}containsElement(e,t){return Ju(e,t)}query(e,t,i){return ed(e,t,i)}computeStyle(e,t,i){return window.getComputedStyle(e)[t]}buildKeyframeElement(e,t,i){i=i.map(e=>td(e));let n=`@keyframes ${t} {\n`,r="";i.forEach(e=>{r=" ";const t=parseFloat(e.offset);n+=`${r}${100*t}% {\n`,r+=" ",Object.keys(e).forEach(t=>{const i=e[t];switch(t){case"offset":return;case"easing":return void(i&&(n+=`${r}animation-timing-function: ${i};\n`));default:return void(n+=`${r}${t}: ${i};\n`)}}),n+=r+"}\n"}),n+="}\n";const s=document.createElement("style");return s.textContent=n,s}animate(e,t,i,n,r,s=[],o){const a=s.filter(e=>e instanceof Wf),l={};Ed(i,n)&&a.forEach(e=>{let t=e.currentSnapshot;Object.keys(t).forEach(e=>l[e]=t[e])});const c=function(e){let t={};return e&&(Array.isArray(e)?e:[e]).forEach(e=>{Object.keys(e).forEach(i=>{"offset"!=i&&"easing"!=i&&(t[i]=e[i])})}),t}(t=Ad(e,t,l));if(0==i)return new $f(e,c);const h="gen_css_kf_"+this._count++,u=this.buildKeyframeElement(e,h,t);document.querySelector("head").appendChild(u);const d=Df(e,t),f=new Wf(e,t,h,i,n,r,c,d);return f.onDestroy(()=>{var e;(e=u).parentNode.removeChild(e)}),f}}class Gf{constructor(e,t,i,n){this.element=e,this.keyframes=t,this.options=i,this._specialStyles=n,this._onDoneFns=[],this._onStartFns=[],this._onDestroyFns=[],this._initialized=!1,this._finished=!1,this._started=!1,this._destroyed=!1,this.time=0,this.parentPlayer=null,this.currentSnapshot={},this._duration=i.duration,this._delay=i.delay||0,this.time=this._duration+this._delay}_onFinish(){this._finished||(this._finished=!0,this._onDoneFns.forEach(e=>e()),this._onDoneFns=[])}init(){this._buildPlayer(),this._preparePlayerBeforeStart()}_buildPlayer(){if(this._initialized)return;this._initialized=!0;const e=this.keyframes;this.domPlayer=this._triggerWebAnimation(this.element,e,this.options),this._finalKeyframe=e.length?e[e.length-1]:{},this.domPlayer.addEventListener("finish",()=>this._onFinish())}_preparePlayerBeforeStart(){this._delay?this._resetDomPlayerState():this.domPlayer.pause()}_triggerWebAnimation(e,t,i){return e.animate(t,i)}onStart(e){this._onStartFns.push(e)}onDone(e){this._onDoneFns.push(e)}onDestroy(e){this._onDestroyFns.push(e)}play(){this._buildPlayer(),this.hasStarted()||(this._onStartFns.forEach(e=>e()),this._onStartFns=[],this._started=!0,this._specialStyles&&this._specialStyles.start()),this.domPlayer.play()}pause(){this.init(),this.domPlayer.pause()}finish(){this.init(),this._specialStyles&&this._specialStyles.finish(),this._onFinish(),this.domPlayer.finish()}reset(){this._resetDomPlayerState(),this._destroyed=!1,this._finished=!1,this._started=!1}_resetDomPlayerState(){this.domPlayer&&this.domPlayer.cancel()}restart(){this.reset(),this.play()}hasStarted(){return this._started}destroy(){this._destroyed||(this._destroyed=!0,this._resetDomPlayerState(),this._onFinish(),this._specialStyles&&this._specialStyles.destroy(),this._onDestroyFns.forEach(e=>e()),this._onDestroyFns=[])}setPosition(e){void 0===this.domPlayer&&this.init(),this.domPlayer.currentTime=e*this.time}getPosition(){return this.domPlayer.currentTime/this.time}get totalTime(){return this._delay+this._duration}beforeDestroy(){const e={};this.hasStarted()&&Object.keys(this._finalKeyframe).forEach(t=>{"offset"!=t&&(e[t]=this._finished?this._finalKeyframe[t]:Od(this.element,t))}),this.currentSnapshot=e}triggerCallback(e){const t="start"==e?this._onStartFns:this._onDoneFns;t.forEach(e=>e()),t.length=0}}class Zf{constructor(){this._isNativeImpl=/\{\s*\[native\s+code\]\s*\}/.test(Yf().toString()),this._cssKeyframesDriver=new Kf}validateStyleProperty(e){return Xu(e)}matchesElement(e,t){return Qu(e,t)}containsElement(e,t){return Ju(e,t)}query(e,t,i){return ed(e,t,i)}computeStyle(e,t,i){return window.getComputedStyle(e)[t]}overrideWebAnimationsSupport(e){this._isNativeImpl=e}animate(e,t,i,n,r,s=[],o){if(!o&&!this._isNativeImpl)return this._cssKeyframesDriver.animate(e,t,i,n,r,s);const a={duration:i,delay:n,fill:0==n?"both":"forwards"};r&&(a.easing=r);const l={},c=s.filter(e=>e instanceof Gf);Ed(i,n)&&c.forEach(e=>{let t=e.currentSnapshot;Object.keys(t).forEach(e=>l[e]=t[e])});const h=Df(e,t=Ad(e,t=t.map(e=>pd(e,!1)),l));return new Gf(e,t,a,h)}}function Yf(){return"undefined"!=typeof window&&void 0!==window.document&&Element.prototype.animate||{}}let Xf=(()=>{class e extends ku{constructor(e,t){super(),this._nextAnimationId=0,this._renderer=e.createRenderer(t.body,{id:"0",encapsulation:Oe.None,styles:[],data:{animation:[]}})}build(e){const t=this._nextAnimationId.toString();this._nextAnimationId++;const i=Array.isArray(e)?Tu(e):e;return ep(this._renderer,null,t,"register",[i]),new Qf(t,this._renderer)}}return e.\u0275fac=function(t){return new(t||e)(mn(Va),mn(ah))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();class Qf extends class{}{constructor(e,t){super(),this._id=e,this._renderer=t}create(e,t){return new Jf(this._id,e,t||{},this._renderer)}}class Jf{constructor(e,t,i,n){this.id=e,this.element=t,this._renderer=n,this.parentPlayer=null,this._started=!1,this.totalTime=0,this._command("create",i)}_listen(e,t){return this._renderer.listen(this.element,`@@${this.id}:${e}`,t)}_command(e,...t){return ep(this._renderer,this.element,this.id,e,t)}onDone(e){this._listen("done",e)}onStart(e){this._listen("start",e)}onDestroy(e){this._listen("destroy",e)}init(){this._command("init")}hasStarted(){return this._started}play(){this._command("play"),this._started=!0}pause(){this._command("pause")}restart(){this._command("restart")}finish(){this._command("finish")}destroy(){this._command("destroy")}reset(){this._command("reset")}setPosition(e){this._command("setPosition",e)}getPosition(){var e,t;return null!==(t=null===(e=this._renderer.engine.players[+this.id])||void 0===e?void 0:e.getPosition())&&void 0!==t?t:0}}function ep(e,t,i,n,r){return e.setProperty(t,`@@${i}:${n}`,r)}const tp="@",ip="@.disabled";let np=(()=>{class e{constructor(e,t,i){this.delegate=e,this.engine=t,this._zone=i,this._currentId=0,this._microtaskId=1,this._animationCallbacksBuffer=[],this._rendererCache=new Map,this._cdRecurDepth=0,this.promise=Promise.resolve(0),t.onRemovalComplete=(e,t)=>{t&&t.parentNode(e)&&t.removeChild(e.parentNode,e)}}createRenderer(e,t){const i=this.delegate.createRenderer(e,t);if(!(e&&t&&t.data&&t.data.animation)){let e=this._rendererCache.get(i);return e||(e=new rp("",i,this.engine),this._rendererCache.set(i,e)),e}const n=t.id,r=t.id+"-"+this._currentId;this._currentId++,this.engine.register(r,e);const s=t=>{Array.isArray(t)?t.forEach(s):this.engine.registerTrigger(n,r,e,t.name,t)};return t.data.animation.forEach(s),new sp(this,r,i,this.engine)}begin(){this._cdRecurDepth++,this.delegate.begin&&this.delegate.begin()}_scheduleCountTask(){this.promise.then(()=>{this._microtaskId++})}scheduleListenerCallback(e,t,i){e>=0&&et(i)):(0==this._animationCallbacksBuffer.length&&Promise.resolve(null).then(()=>{this._zone.run(()=>{this._animationCallbacksBuffer.forEach(e=>{const[t,i]=e;t(i)}),this._animationCallbacksBuffer=[]})}),this._animationCallbacksBuffer.push([t,i]))}end(){this._cdRecurDepth--,0==this._cdRecurDepth&&this._zone.runOutsideAngular(()=>{this._scheduleCountTask(),this.engine.flush(this._microtaskId)}),this.delegate.end&&this.delegate.end()}whenRenderingDone(){return this.engine.whenRenderingDone()}}return e.\u0275fac=function(t){return new(t||e)(mn(Va),mn(Lf),mn(Tc))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();class rp{constructor(e,t,i){this.namespaceId=e,this.delegate=t,this.engine=i,this.destroyNode=this.delegate.destroyNode?e=>t.destroyNode(e):null}get data(){return this.delegate.data}destroy(){this.engine.destroy(this.namespaceId,this.delegate),this.delegate.destroy()}createElement(e,t){return this.delegate.createElement(e,t)}createComment(e){return this.delegate.createComment(e)}createText(e){return this.delegate.createText(e)}appendChild(e,t){this.delegate.appendChild(e,t),this.engine.onInsert(this.namespaceId,t,e,!1)}insertBefore(e,t,i,n=!0){this.delegate.insertBefore(e,t,i),this.engine.onInsert(this.namespaceId,t,e,n)}removeChild(e,t,i){this.engine.onRemove(this.namespaceId,t,this.delegate,i)}selectRootElement(e,t){return this.delegate.selectRootElement(e,t)}parentNode(e){return this.delegate.parentNode(e)}nextSibling(e){return this.delegate.nextSibling(e)}setAttribute(e,t,i,n){this.delegate.setAttribute(e,t,i,n)}removeAttribute(e,t,i){this.delegate.removeAttribute(e,t,i)}addClass(e,t){this.delegate.addClass(e,t)}removeClass(e,t){this.delegate.removeClass(e,t)}setStyle(e,t,i,n){this.delegate.setStyle(e,t,i,n)}removeStyle(e,t,i){this.delegate.removeStyle(e,t,i)}setProperty(e,t,i){t.charAt(0)==tp&&t==ip?this.disableAnimations(e,!!i):this.delegate.setProperty(e,t,i)}setValue(e,t){this.delegate.setValue(e,t)}listen(e,t,i){return this.delegate.listen(e,t,i)}disableAnimations(e,t){this.engine.disableAnimations(e,t)}}class sp extends rp{constructor(e,t,i,n){super(t,i,n),this.factory=e,this.namespaceId=t}setProperty(e,t,i){t.charAt(0)==tp?"."==t.charAt(1)&&t==ip?this.disableAnimations(e,i=void 0===i||!!i):this.engine.process(this.namespaceId,e,t.substr(1),i):this.delegate.setProperty(e,t,i)}listen(e,t,i){if(t.charAt(0)==tp){const n=function(e){switch(e){case"body":return document.body;case"document":return document;case"window":return window;default:return e}}(e);let r=t.substr(1),s="";return r.charAt(0)!=tp&&([r,s]=function(e){const t=e.indexOf(".");return[e.substring(0,t),e.substr(t+1)]}(r)),this.engine.listen(this.namespaceId,n,r,s,e=>{this.factory.scheduleListenerCallback(e._data||-1,i,e)})}return this.delegate.listen(e,t,i)}}let op=(()=>{class e extends Lf{constructor(e,t,i){super(e.body,t,i)}}return e.\u0275fac=function(t){return new(t||e)(mn(ah),mn(nd),mn(Jd))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const ap=new Qi("AnimationModuleType"),lp=[{provide:nd,useFactory:function(){return"function"==typeof Yf()?new Zf:new Kf}},{provide:ap,useValue:"BrowserAnimations"},{provide:ku,useClass:Xf},{provide:Jd,useFactory:function(){return new ef}},{provide:Lf,useClass:op},{provide:Va,useFactory:function(e,t,i){return new np(e,t,i)},deps:[uu,Lf,Tc]}];let cp=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:lp,imports:[Su]}),e})();const hp=new v(e=>e.complete());function up(e){return e?function(e){return new v(t=>e.schedule(()=>t.complete()))}(e):hp}function dp(e){return new v(t=>{let i;try{i=e()}catch(n){return void t.error(n)}return(i?j(i):up()).subscribe(t)})}function fp(e,t){return new v(t?i=>t.schedule(pp,0,{error:e,subscriber:i}):t=>t.error(e))}function pp({error:e,subscriber:t}){t.error(e)}function _p(e,t,i,r){return n(i)&&(r=i,i=void 0),r?_p(e,t,i).pipe(M(e=>l(e)?r(...e):r(e))):new v(n=>{!function e(t,i,n,r,s){let o;if(function(e){return e&&"function"==typeof e.addEventListener&&"function"==typeof e.removeEventListener}(t)){const e=t;t.addEventListener(i,n,s),o=()=>e.removeEventListener(i,n,s)}else if(function(e){return e&&"function"==typeof e.on&&"function"==typeof e.off}(t)){const e=t;t.on(i,n),o=()=>e.off(i,n)}else if(function(e){return e&&"function"==typeof e.addListener&&"function"==typeof e.removeListener}(t)){const e=t;t.addListener(i,n),o=()=>e.removeListener(i,n)}else{if(!t||!t.length)throw new TypeError("Invalid event target");for(let o=0,a=t.length;o1?Array.prototype.slice.call(arguments):e)}),n,i)})}function mp(...e){let t=e[e.length-1];return x(t)?(e.pop(),B(e,t)):z(e)}function gp(...e){return q(1)(mp(...e))}function vp(){}const yp=new v(vp);function bp(e,t){return function(i){return i.lift(new Cp(e,t))}}class Cp{constructor(e,t){this.predicate=e,this.thisArg=t}call(e,t){return t.subscribe(new wp(e,this.predicate,this.thisArg))}}class wp extends p{constructor(e,t,i){super(e),this.predicate=t,this.thisArg=i,this.count=0}_next(e){let t;try{t=this.predicate.call(this.thisArg,e,this.count++)}catch(i){return void this.destination.error(i)}t&&this.destination.next(e)}}function Sp(e,t){return"function"==typeof t?i=>i.pipe(Sp((i,n)=>j(e(i,n)).pipe(M((e,r)=>t(i,e,n,r))))):t=>t.lift(new kp(e))}class kp{constructor(e){this.project=e}call(e,t){return t.subscribe(new xp(e,this.project))}}class xp extends I{constructor(e,t){super(e),this.project=t,this.index=0}_next(e){let t;const i=this.index++;try{t=this.project(e,i)}catch(n){return void this.destination.error(n)}this._innerSub(t,e,i)}_innerSub(e,t,i){const n=this.innerSubscription;n&&n.unsubscribe();const r=new E(this,t,i),s=this.destination;s.add(r),this.innerSubscription=P(this,e,void 0,void 0,r),this.innerSubscription!==r&&s.add(this.innerSubscription)}_complete(){const{innerSubscription:e}=this;e&&!e.closed||super._complete(),this.unsubscribe()}_unsubscribe(){this.innerSubscription=null}notifyComplete(e){this.destination.remove(e),this.innerSubscription=null,this.isStopped&&super._complete()}notifyNext(e,t,i,n,r){this.destination.next(t)}}const Ep=(()=>{function e(){return Error.call(this),this.message="argument out of range",this.name="ArgumentOutOfRangeError",this}return e.prototype=Object.create(Error.prototype),e})();function Ap(e){return t=>0===e?up():t.lift(new Tp(e))}class Tp{constructor(e){if(this.total=e,this.total<0)throw new Ep}call(e,t){return t.subscribe(new Op(e,this.total))}}class Op extends p{constructor(e,t){super(e),this.total=t,this.count=0}_next(e){const t=this.total,i=++this.count;i<=t&&(this.destination.next(e),i===t&&(this.destination.complete(),this.unsubscribe()))}}function Rp(e,t,i){return function(n){return n.lift(new Lp(e,t,i))}}class Lp{constructor(e,t,i){this.nextOrObserver=e,this.error=t,this.complete=i}call(e,t){return t.subscribe(new Dp(e,this.nextOrObserver,this.error,this.complete))}}class Dp extends p{constructor(e,t,i,r){super(e),this._tapNext=vp,this._tapError=vp,this._tapComplete=vp,this._tapError=i||vp,this._tapComplete=r||vp,n(t)?(this._context=this,this._tapNext=t):t&&(this._context=t,this._tapNext=t.next||vp,this._tapError=t.error||vp,this._tapComplete=t.complete||vp)}_next(e){try{this._tapNext.call(this._context,e)}catch(t){return void this.destination.error(t)}this.destination.next(e)}_error(e){try{this._tapError.call(this._context,e)}catch(e){return void this.destination.error(e)}this.destination.error(e)}_complete(){try{this._tapComplete.call(this._context)}catch(e){return void this.destination.error(e)}return this.destination.complete()}}class Pp extends u{constructor(e,t){super()}schedule(e,t=0){return this}}class Ip extends Pp{constructor(e,t){super(e,t),this.scheduler=e,this.work=t,this.pending=!1}schedule(e,t=0){if(this.closed)return this;this.state=e;const i=this.id,n=this.scheduler;return null!=i&&(this.id=this.recycleAsyncId(n,i,t)),this.pending=!0,this.delay=t,this.id=this.id||this.requestAsyncId(n,this.id,t),this}requestAsyncId(e,t,i=0){return setInterval(e.flush.bind(e,this),i)}recycleAsyncId(e,t,i=0){if(null!==i&&this.delay===i&&!1===this.pending)return t;clearInterval(t)}execute(e,t){if(this.closed)return new Error("executing a cancelled action");this.pending=!1;const i=this._execute(e,t);if(i)return i;!1===this.pending&&null!=this.id&&(this.id=this.recycleAsyncId(this.scheduler,this.id,null))}_execute(e,t){let i=!1,n=void 0;try{this.work(e)}catch(r){i=!0,n=!!r&&r||new Error(r)}if(i)return this.unsubscribe(),n}_unsubscribe(){const e=this.id,t=this.scheduler,i=t.actions,n=i.indexOf(this);this.work=null,this.state=null,this.pending=!1,this.scheduler=null,-1!==n&&i.splice(n,1),null!=e&&(this.id=this.recycleAsyncId(t,e,null)),this.delay=null}}let Mp=(()=>{class e{constructor(t,i=e.now){this.SchedulerAction=t,this.now=i}schedule(e,t=0,i){return new this.SchedulerAction(this,e).schedule(i,t)}}return e.now=()=>Date.now(),e})();class Fp extends Mp{constructor(e,t=Mp.now){super(e,()=>Fp.delegate&&Fp.delegate!==this?Fp.delegate.now():t()),this.actions=[],this.active=!1,this.scheduled=void 0}schedule(e,t=0,i){return Fp.delegate&&Fp.delegate!==this?Fp.delegate.schedule(e,t,i):super.schedule(e,t,i)}flush(e){const{actions:t}=this;if(this.active)return void t.push(e);let i;this.active=!0;do{if(i=e.execute(e.state,e.delay))break}while(e=t.shift());if(this.active=!1,i){for(;e=t.shift();)e.unsubscribe();throw i}}}const Hp=new Fp(Ip);let Bp=(()=>{class e{constructor(e,t,i){this.kind=e,this.value=t,this.error=i,this.hasValue="N"===e}observe(e){switch(this.kind){case"N":return e.next&&e.next(this.value);case"E":return e.error&&e.error(this.error);case"C":return e.complete&&e.complete()}}do(e,t,i){switch(this.kind){case"N":return e&&e(this.value);case"E":return t&&t(this.error);case"C":return i&&i()}}accept(e,t,i){return e&&"function"==typeof e.next?this.observe(e):this.do(e,t,i)}toObservable(){switch(this.kind){case"N":return mp(this.value);case"E":return fp(this.error);case"C":return up()}throw new Error("unexpected notification kind value")}static createNext(t){return void 0!==t?new e("N",t):e.undefinedValueNotification}static createError(t){return new e("E",void 0,t)}static createComplete(){return e.completeNotification}}return e.completeNotification=new e("C"),e.undefinedValueNotification=new e("N",void 0),e})();class jp{constructor(e,t){this.delay=e,this.scheduler=t}call(e,t){return t.subscribe(new Np(e,this.delay,this.scheduler))}}class Np extends p{constructor(e,t,i){super(e),this.delay=t,this.scheduler=i,this.queue=[],this.active=!1,this.errored=!1}static dispatch(e){const t=e.source,i=t.queue,n=e.scheduler,r=e.destination;for(;i.length>0&&i[0].time-n.now()<=0;)i.shift().notification.observe(r);if(i.length>0){const t=Math.max(0,i[0].time-n.now());this.schedule(e,t)}else this.unsubscribe(),t.active=!1}_schedule(e){this.active=!0,this.destination.add(e.schedule(Np.dispatch,this.delay,{source:this,destination:this.destination,scheduler:e}))}scheduleNotification(e){if(!0===this.errored)return;const t=this.scheduler,i=new Vp(t.now()+this.delay,e);this.queue.push(i),!1===this.active&&this._schedule(t)}_next(e){this.scheduleNotification(Bp.createNext(e))}_error(e){this.errored=!0,this.queue=[],this.destination.error(e),this.unsubscribe()}_complete(){this.scheduleNotification(Bp.createComplete()),this.unsubscribe()}}class Vp{constructor(e,t){this.time=e,this.notification=t}}const Up="Service workers are disabled or not supported by this browser";class qp{constructor(e){if(this.serviceWorker=e,e){const t=_p(e,"controllerchange").pipe(M(()=>e.controller)),i=gp(dp(()=>mp(e.controller)),t);this.worker=i.pipe(bp(e=>!!e)),this.registration=this.worker.pipe(Sp(()=>e.getRegistration()));const n=_p(e,"message").pipe(M(e=>e.data)).pipe(bp(e=>e&&e.type)).pipe(Q(new S));n.connect(),this.events=n}else this.worker=this.events=this.registration=dp(()=>fp(new Error("Service workers are disabled or not supported by this browser")))}postMessage(e,t){return this.worker.pipe(Ap(1),Rp(i=>{i.postMessage(Object.assign({action:e},t))})).toPromise().then(()=>{})}postMessageWithStatus(e,t,i){const n=this.waitForStatus(i),r=this.postMessage(e,t);return Promise.all([n,r]).then(()=>{})}generateNonce(){return Math.round(1e7*Math.random())}eventsOfType(e){return this.events.pipe(bp(t=>t.type===e))}nextEventOfType(e){return this.eventsOfType(e).pipe(Ap(1))}waitForStatus(e){return this.eventsOfType("STATUS").pipe(bp(t=>t.nonce===e),Ap(1),M(e=>{if(!e.status)throw new Error(e.error)})).toPromise()}get isEnabled(){return!!this.serviceWorker}}let zp=(()=>{class e{constructor(e){if(this.sw=e,this.subscriptionChanges=new S,!e.isEnabled)return this.messages=yp,this.notificationClicks=yp,void(this.subscription=yp);this.messages=this.sw.eventsOfType("PUSH").pipe(M(e=>e.data)),this.notificationClicks=this.sw.eventsOfType("NOTIFICATION_CLICK").pipe(M(e=>e.data)),this.pushManager=this.sw.registration.pipe(M(e=>e.pushManager));const t=this.pushManager.pipe(Sp(e=>e.getSubscription()));this.subscription=W(t,this.subscriptionChanges)}get isEnabled(){return this.sw.isEnabled}requestSubscription(e){if(!this.sw.isEnabled)return Promise.reject(new Error(Up));const t={userVisibleOnly:!0};let i=this.decodeBase64(e.serverPublicKey.replace(/_/g,"/").replace(/-/g,"+")),n=new Uint8Array(new ArrayBuffer(i.length));for(let r=0;re.subscribe(t)),Ap(1)).toPromise().then(e=>(this.subscriptionChanges.next(e),e))}unsubscribe(){return this.sw.isEnabled?this.subscription.pipe(Ap(1),Sp(e=>{if(null===e)throw new Error("Not subscribed to push notifications.");return e.unsubscribe().then(e=>{if(!e)throw new Error("Unsubscribe failed!");this.subscriptionChanges.next(null)})})).toPromise():Promise.reject(new Error(Up))}decodeBase64(e){return atob(e)}}return e.\u0275fac=function(t){return new(t||e)(mn(qp))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),Wp=(()=>{class e{constructor(e){if(this.sw=e,!e.isEnabled)return this.available=yp,this.activated=yp,void(this.unrecoverable=yp);this.available=this.sw.eventsOfType("UPDATE_AVAILABLE"),this.activated=this.sw.eventsOfType("UPDATE_ACTIVATED"),this.unrecoverable=this.sw.eventsOfType("UNRECOVERABLE_STATE")}get isEnabled(){return this.sw.isEnabled}checkForUpdate(){if(!this.sw.isEnabled)return Promise.reject(new Error(Up));const e=this.sw.generateNonce();return this.sw.postMessageWithStatus("CHECK_FOR_UPDATES",{statusNonce:e},e)}activateUpdate(){if(!this.sw.isEnabled)return Promise.reject(new Error(Up));const e=this.sw.generateNonce();return this.sw.postMessageWithStatus("ACTIVATE_UPDATE",{statusNonce:e},e)}}return e.\u0275fac=function(t){return new(t||e)(mn(qp))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();class $p{}const Kp=new Qi("NGSW_REGISTER_SCRIPT");function Gp(e,t,i,n){return()=>{if(!$h(n)||!("serviceWorker"in navigator)||!1===i.enabled)return;let r;if(navigator.serviceWorker.addEventListener("controllerchange",()=>{null!==navigator.serviceWorker.controller&&navigator.serviceWorker.controller.postMessage({action:"INITIALIZE"})}),"function"==typeof i.registrationStrategy)r=i.registrationStrategy();else{const[t,...n]=(i.registrationStrategy||"registerWhenStable:30000").split(":");switch(t){case"registerImmediately":r=mp(null);break;case"registerWithDelay":r=Zp(+n[0]||0);break;case"registerWhenStable":r=n[0]?W(Yp(e),Zp(+n[0])):Yp(e);break;default:throw new Error("Unknown ServiceWorker registration strategy: "+i.registrationStrategy)}}e.get(Tc).runOutsideAngular(()=>r.pipe(Ap(1)).subscribe(()=>navigator.serviceWorker.register(t,{scope:i.scope}).catch(e=>console.error("Service worker registration failed with:",e))))}}function Zp(e){return mp(null).pipe(function(e,t=Hp){var i;const n=(i=e)instanceof Date&&!isNaN(+i)?+e-t.now():Math.abs(e);return e=>e.lift(new jp(n,t))}(e))}function Yp(e){return e.get(Zc).isStable.pipe(bp(e=>e))}function Xp(e,t){return new qp($h(t)&&!1!==e.enabled?navigator.serviceWorker:void 0)}let Qp=(()=>{class e{static register(t,i={}){return{ngModule:e,providers:[{provide:Kp,useValue:t},{provide:$p,useValue:i},{provide:qp,useFactory:Xp,deps:[$p,fc]},{provide:ac,useFactory:Gp,deps:[oo,Kp,$p,fc],multi:!0}]}}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[zp,Wp]}),e})();function Jp(e,t){return N(e,t,1)}class e_{}class t_{}class i_{constructor(e){this.normalizedNames=new Map,this.lazyUpdate=null,e?this.lazyInit="string"==typeof e?()=>{this.headers=new Map,e.split("\n").forEach(e=>{const t=e.indexOf(":");if(t>0){const i=e.slice(0,t),n=i.toLowerCase(),r=e.slice(t+1).trim();this.maybeSetNormalizedName(i,n),this.headers.has(n)?this.headers.get(n).push(r):this.headers.set(n,[r])}})}:()=>{this.headers=new Map,Object.keys(e).forEach(t=>{let i=e[t];const n=t.toLowerCase();"string"==typeof i&&(i=[i]),i.length>0&&(this.headers.set(n,i),this.maybeSetNormalizedName(t,n))})}:this.headers=new Map}has(e){return this.init(),this.headers.has(e.toLowerCase())}get(e){this.init();const t=this.headers.get(e.toLowerCase());return t&&t.length>0?t[0]:null}keys(){return this.init(),Array.from(this.normalizedNames.values())}getAll(e){return this.init(),this.headers.get(e.toLowerCase())||null}append(e,t){return this.clone({name:e,value:t,op:"a"})}set(e,t){return this.clone({name:e,value:t,op:"s"})}delete(e,t){return this.clone({name:e,value:t,op:"d"})}maybeSetNormalizedName(e,t){this.normalizedNames.has(t)||this.normalizedNames.set(t,e)}init(){this.lazyInit&&(this.lazyInit instanceof i_?this.copyFrom(this.lazyInit):this.lazyInit(),this.lazyInit=null,this.lazyUpdate&&(this.lazyUpdate.forEach(e=>this.applyUpdate(e)),this.lazyUpdate=null))}copyFrom(e){e.init(),Array.from(e.headers.keys()).forEach(t=>{this.headers.set(t,e.headers.get(t)),this.normalizedNames.set(t,e.normalizedNames.get(t))})}clone(e){const t=new i_;return t.lazyInit=this.lazyInit&&this.lazyInit instanceof i_?this.lazyInit:this,t.lazyUpdate=(this.lazyUpdate||[]).concat([e]),t}applyUpdate(e){const t=e.name.toLowerCase();switch(e.op){case"a":case"s":let i=e.value;if("string"==typeof i&&(i=[i]),0===i.length)return;this.maybeSetNormalizedName(e.name,t);const n=("a"===e.op?this.headers.get(t):void 0)||[];n.push(...i),this.headers.set(t,n);break;case"d":const r=e.value;if(r){let e=this.headers.get(t);if(!e)return;e=e.filter(e=>-1===r.indexOf(e)),0===e.length?(this.headers.delete(t),this.normalizedNames.delete(t)):this.headers.set(t,e)}else this.headers.delete(t),this.normalizedNames.delete(t)}}forEach(e){this.init(),Array.from(this.normalizedNames.keys()).forEach(t=>e(this.normalizedNames.get(t),this.headers.get(t)))}}class n_{encodeKey(e){return r_(e)}encodeValue(e){return r_(e)}decodeKey(e){return decodeURIComponent(e)}decodeValue(e){return decodeURIComponent(e)}}function r_(e){return encodeURIComponent(e).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/gi,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%2B/gi,"+").replace(/%3D/gi,"=").replace(/%3F/gi,"?").replace(/%2F/gi,"/")}class s_{constructor(e={}){if(this.updates=null,this.cloneFrom=null,this.encoder=e.encoder||new n_,e.fromString){if(e.fromObject)throw new Error("Cannot specify both fromString and fromObject.");this.map=function(e,t){const i=new Map;return e.length>0&&e.replace(/^\?/,"").split("&").forEach(e=>{const n=e.indexOf("="),[r,s]=-1==n?[t.decodeKey(e),""]:[t.decodeKey(e.slice(0,n)),t.decodeValue(e.slice(n+1))],o=i.get(r)||[];o.push(s),i.set(r,o)}),i}(e.fromString,this.encoder)}else e.fromObject?(this.map=new Map,Object.keys(e.fromObject).forEach(t=>{const i=e.fromObject[t];this.map.set(t,Array.isArray(i)?i:[i])})):this.map=null}has(e){return this.init(),this.map.has(e)}get(e){this.init();const t=this.map.get(e);return t?t[0]:null}getAll(e){return this.init(),this.map.get(e)||null}keys(){return this.init(),Array.from(this.map.keys())}append(e,t){return this.clone({param:e,value:t,op:"a"})}appendAll(e){const t=[];return Object.keys(e).forEach(i=>{const n=e[i];Array.isArray(n)?n.forEach(e=>{t.push({param:i,value:e,op:"a"})}):t.push({param:i,value:n,op:"a"})}),this.clone(t)}set(e,t){return this.clone({param:e,value:t,op:"s"})}delete(e,t){return this.clone({param:e,value:t,op:"d"})}toString(){return this.init(),this.keys().map(e=>{const t=this.encoder.encodeKey(e);return this.map.get(e).map(e=>t+"="+this.encoder.encodeValue(e)).join("&")}).filter(e=>""!==e).join("&")}clone(e){const t=new s_({encoder:this.encoder});return t.cloneFrom=this.cloneFrom||this,t.updates=(this.updates||[]).concat(e),t}init(){null===this.map&&(this.map=new Map),null!==this.cloneFrom&&(this.cloneFrom.init(),this.cloneFrom.keys().forEach(e=>this.map.set(e,this.cloneFrom.map.get(e))),this.updates.forEach(e=>{switch(e.op){case"a":case"s":const t=("a"===e.op?this.map.get(e.param):void 0)||[];t.push(e.value),this.map.set(e.param,t);break;case"d":if(void 0===e.value){this.map.delete(e.param);break}{let t=this.map.get(e.param)||[];const i=t.indexOf(e.value);-1!==i&&t.splice(i,1),t.length>0?this.map.set(e.param,t):this.map.delete(e.param)}}}),this.cloneFrom=this.updates=null)}}function o_(e){return"undefined"!=typeof ArrayBuffer&&e instanceof ArrayBuffer}function a_(e){return"undefined"!=typeof Blob&&e instanceof Blob}function l_(e){return"undefined"!=typeof FormData&&e instanceof FormData}class c_{constructor(e,t,i,n){let r;if(this.url=t,this.body=null,this.reportProgress=!1,this.withCredentials=!1,this.responseType="json",this.method=e.toUpperCase(),function(e){switch(e){case"DELETE":case"GET":case"HEAD":case"OPTIONS":case"JSONP":return!1;default:return!0}}(this.method)||n?(this.body=void 0!==i?i:null,r=n):r=i,r&&(this.reportProgress=!!r.reportProgress,this.withCredentials=!!r.withCredentials,r.responseType&&(this.responseType=r.responseType),r.headers&&(this.headers=r.headers),r.params&&(this.params=r.params)),this.headers||(this.headers=new i_),this.params){const e=this.params.toString();if(0===e.length)this.urlWithParams=t;else{const i=t.indexOf("?");this.urlWithParams=t+(-1===i?"?":it.set(i,e.setHeaders[i]),a)),e.setParams&&(l=Object.keys(e.setParams).reduce((t,i)=>t.set(i,e.setParams[i]),l)),new c_(t,i,r,{params:l,headers:a,reportProgress:o,responseType:n,withCredentials:s})}}var h_=function(e){return e[e.Sent=0]="Sent",e[e.UploadProgress=1]="UploadProgress",e[e.ResponseHeader=2]="ResponseHeader",e[e.DownloadProgress=3]="DownloadProgress",e[e.Response=4]="Response",e[e.User=5]="User",e}({});class u_{constructor(e,t=200,i="OK"){this.headers=e.headers||new i_,this.status=void 0!==e.status?e.status:t,this.statusText=e.statusText||i,this.url=e.url||null,this.ok=this.status>=200&&this.status<300}}class d_ extends u_{constructor(e={}){super(e),this.type=h_.ResponseHeader}clone(e={}){return new d_({headers:e.headers||this.headers,status:void 0!==e.status?e.status:this.status,statusText:e.statusText||this.statusText,url:e.url||this.url||void 0})}}class f_ extends u_{constructor(e={}){super(e),this.type=h_.Response,this.body=void 0!==e.body?e.body:null}clone(e={}){return new f_({body:void 0!==e.body?e.body:this.body,headers:e.headers||this.headers,status:void 0!==e.status?e.status:this.status,statusText:e.statusText||this.statusText,url:e.url||this.url||void 0})}}class p_ extends u_{constructor(e){super(e,0,"Unknown Error"),this.name="HttpErrorResponse",this.ok=!1,this.message=this.status>=200&&this.status<300?"Http failure during parsing for "+(e.url||"(unknown url)"):`Http failure response for ${e.url||"(unknown url)"}: ${e.status} ${e.statusText}`,this.error=e.error||null}}function __(e,t){return{body:t,headers:e.headers,observe:e.observe,params:e.params,reportProgress:e.reportProgress,responseType:e.responseType,withCredentials:e.withCredentials}}let m_=(()=>{class e{constructor(e){this.handler=e}request(e,t,i={}){let n;if(e instanceof c_)n=e;else{let r=void 0;r=i.headers instanceof i_?i.headers:new i_(i.headers);let s=void 0;i.params&&(s=i.params instanceof s_?i.params:new s_({fromObject:i.params})),n=new c_(e,t,void 0!==i.body?i.body:null,{headers:r,params:s,reportProgress:i.reportProgress,responseType:i.responseType||"json",withCredentials:i.withCredentials})}const r=mp(n).pipe(Jp(e=>this.handler.handle(e)));if(e instanceof c_||"events"===i.observe)return r;const s=r.pipe(bp(e=>e instanceof f_));switch(i.observe||"body"){case"body":switch(n.responseType){case"arraybuffer":return s.pipe(M(e=>{if(null!==e.body&&!(e.body instanceof ArrayBuffer))throw new Error("Response is not an ArrayBuffer.");return e.body}));case"blob":return s.pipe(M(e=>{if(null!==e.body&&!(e.body instanceof Blob))throw new Error("Response is not a Blob.");return e.body}));case"text":return s.pipe(M(e=>{if(null!==e.body&&"string"!=typeof e.body)throw new Error("Response is not a string.");return e.body}));case"json":default:return s.pipe(M(e=>e.body))}case"response":return s;default:throw new Error(`Unreachable: unhandled observe type ${i.observe}}`)}}delete(e,t={}){return this.request("DELETE",e,t)}get(e,t={}){return this.request("GET",e,t)}head(e,t={}){return this.request("HEAD",e,t)}jsonp(e,t){return this.request("JSONP",e,{params:(new s_).append(t,"JSONP_CALLBACK"),observe:"body",responseType:"json"})}options(e,t={}){return this.request("OPTIONS",e,t)}patch(e,t,i={}){return this.request("PATCH",e,__(i,t))}post(e,t,i={}){return this.request("POST",e,__(i,t))}put(e,t,i={}){return this.request("PUT",e,__(i,t))}}return e.\u0275fac=function(t){return new(t||e)(mn(e_))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();class g_{constructor(e,t){this.next=e,this.interceptor=t}handle(e){return this.interceptor.intercept(e,this.next)}}const v_=new Qi("HTTP_INTERCEPTORS");let y_=(()=>{class e{intercept(e,t){return t.handle(e)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const b_=/^\)\]\}',?\n/;class C_{}let w_=(()=>{class e{constructor(){}build(){return new XMLHttpRequest}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),S_=(()=>{class e{constructor(e){this.xhrFactory=e}handle(e){if("JSONP"===e.method)throw new Error("Attempted to construct Jsonp request without HttpClientJsonpModule installed.");return new v(t=>{const i=this.xhrFactory.build();if(i.open(e.method,e.urlWithParams),e.withCredentials&&(i.withCredentials=!0),e.headers.forEach((e,t)=>i.setRequestHeader(e,t.join(","))),e.headers.has("Accept")||i.setRequestHeader("Accept","application/json, text/plain, */*"),!e.headers.has("Content-Type")){const t=e.detectContentTypeHeader();null!==t&&i.setRequestHeader("Content-Type",t)}if(e.responseType){const t=e.responseType.toLowerCase();i.responseType="json"!==t?t:"text"}const n=e.serializeBody();let r=null;const s=()=>{if(null!==r)return r;const t=1223===i.status?204:i.status,n=i.statusText||"OK",s=new i_(i.getAllResponseHeaders()),o=function(e){return"responseURL"in e&&e.responseURL?e.responseURL:/^X-Request-URL:/m.test(e.getAllResponseHeaders())?e.getResponseHeader("X-Request-URL"):null}(i)||e.url;return r=new d_({headers:s,status:t,statusText:n,url:o}),r},o=()=>{let{headers:n,status:r,statusText:o,url:a}=s(),l=null;204!==r&&(l=void 0===i.response?i.responseText:i.response),0===r&&(r=l?200:0);let c=r>=200&&r<300;if("json"===e.responseType&&"string"==typeof l){const e=l;l=l.replace(b_,"");try{l=""!==l?JSON.parse(l):null}catch(h){l=e,c&&(c=!1,l={error:h,text:l})}}c?(t.next(new f_({body:l,headers:n,status:r,statusText:o,url:a||void 0})),t.complete()):t.error(new p_({error:l,headers:n,status:r,statusText:o,url:a||void 0}))},a=e=>{const{url:n}=s(),r=new p_({error:e,status:i.status||0,statusText:i.statusText||"Unknown Error",url:n||void 0});t.error(r)};let l=!1;const c=n=>{l||(t.next(s()),l=!0);let r={type:h_.DownloadProgress,loaded:n.loaded};n.lengthComputable&&(r.total=n.total),"text"===e.responseType&&i.responseText&&(r.partialText=i.responseText),t.next(r)},h=e=>{let i={type:h_.UploadProgress,loaded:e.loaded};e.lengthComputable&&(i.total=e.total),t.next(i)};return i.addEventListener("load",o),i.addEventListener("error",a),i.addEventListener("timeout",a),i.addEventListener("abort",a),e.reportProgress&&(i.addEventListener("progress",c),null!==n&&i.upload&&i.upload.addEventListener("progress",h)),i.send(n),t.next({type:h_.Sent}),()=>{i.removeEventListener("error",a),i.removeEventListener("abort",a),i.removeEventListener("load",o),i.removeEventListener("timeout",a),e.reportProgress&&(i.removeEventListener("progress",c),null!==n&&i.upload&&i.upload.removeEventListener("progress",h)),i.readyState!==i.DONE&&i.abort()}})}}return e.\u0275fac=function(t){return new(t||e)(mn(C_))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const k_=new Qi("XSRF_COOKIE_NAME"),x_=new Qi("XSRF_HEADER_NAME");class E_{}let A_=(()=>{class e{constructor(e,t,i){this.doc=e,this.platform=t,this.cookieName=i,this.lastCookieString="",this.lastToken=null,this.parseCount=0}getToken(){if("server"===this.platform)return null;const e=this.doc.cookie||"";return e!==this.lastCookieString&&(this.parseCount++,this.lastToken=Th(e,this.cookieName),this.lastCookieString=e),this.lastToken}}return e.\u0275fac=function(t){return new(t||e)(mn(ah),mn(fc),mn(k_))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),T_=(()=>{class e{constructor(e,t){this.tokenService=e,this.headerName=t}intercept(e,t){const i=e.url.toLowerCase();if("GET"===e.method||"HEAD"===e.method||i.startsWith("http://")||i.startsWith("https://"))return t.handle(e);const n=this.tokenService.getToken();return null===n||e.headers.has(this.headerName)||(e=e.clone({headers:e.headers.set(this.headerName,n)})),t.handle(e)}}return e.\u0275fac=function(t){return new(t||e)(mn(E_),mn(x_))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),O_=(()=>{class e{constructor(e,t){this.backend=e,this.injector=t,this.chain=null}handle(e){if(null===this.chain){const e=this.injector.get(v_,[]);this.chain=e.reduceRight((e,t)=>new g_(e,t),this.backend)}return this.chain.handle(e)}}return e.\u0275fac=function(t){return new(t||e)(mn(t_),mn(oo))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),R_=(()=>{class e{static disable(){return{ngModule:e,providers:[{provide:T_,useClass:y_}]}}static withOptions(t={}){return{ngModule:e,providers:[t.cookieName?{provide:k_,useValue:t.cookieName}:[],t.headerName?{provide:x_,useValue:t.headerName}:[]]}}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[T_,{provide:v_,useExisting:T_,multi:!0},{provide:E_,useClass:A_},{provide:k_,useValue:"XSRF-TOKEN"},{provide:x_,useValue:"X-XSRF-TOKEN"}]}),e})(),L_=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[m_,{provide:e_,useClass:O_},S_,{provide:t_,useExisting:S_},w_,{provide:C_,useExisting:w_}],imports:[[R_.withOptions({cookieName:"XSRF-TOKEN",headerName:"X-XSRF-TOKEN"})]]}),e})();function D_(e){return null!=e&&""+e!="false"}function P_(e,t=0){return function(e){return!isNaN(parseFloat(e))&&!isNaN(Number(e))}(e)?Number(e):t}function I_(e){return Array.isArray(e)?e:[e]}function M_(e){return null==e?"":"string"==typeof e?e:e+"px"}function F_(e){return e instanceof ja?e.nativeElement:e}class H_{constructor(e,t){this.compare=e,this.keySelector=t}call(e,t){return t.subscribe(new B_(e,this.compare,this.keySelector))}}class B_ extends p{constructor(e,t,i){super(e),this.keySelector=i,this.hasKey=!1,"function"==typeof t&&(this.compare=t)}compare(e,t){return e===t}_next(e){let t;try{const{keySelector:i}=this;t=i?i(e):e}catch(n){return this.destination.error(n)}let i=!1;if(this.hasKey)try{const{compare:e}=this;i=e(this.key,t)}catch(n){return this.destination.error(n)}else this.hasKey=!0;i||(this.key=t,this.destination.next(e))}}class j_{constructor(e){this.durationSelector=e}call(e,t){return t.subscribe(new N_(e,this.durationSelector))}}class N_ extends I{constructor(e,t){super(e),this.durationSelector=t,this.hasValue=!1}_next(e){if(this.value=e,this.hasValue=!0,!this.throttled){let i;try{const{durationSelector:t}=this;i=t(e)}catch(t){return this.destination.error(t)}const n=P(this,i);!n||n.closed?this.clearThrottle():this.add(this.throttled=n)}}clearThrottle(){const{value:e,hasValue:t,throttled:i}=this;i&&(this.remove(i),this.throttled=null,i.unsubscribe()),t&&(this.value=null,this.hasValue=!1,this.destination.next(e))}notifyNext(e,t,i,n){this.clearThrottle()}notifyComplete(){this.clearThrottle()}}function V_(e){return!l(e)&&e-parseFloat(e)+1>=0}function U_(e){const{index:t,period:i,subscriber:n}=e;if(n.next(t),!n.closed){if(-1===i)return n.complete();e.index=t+1,this.schedule(e,i)}}function q_(e,t=Hp){return i=()=>function(e=0,t,i){let n=-1;return V_(t)?n=Number(t)<1?1:Number(t):x(t)&&(i=t),x(i)||(i=Hp),new v(t=>{const r=V_(e)?e:+e-i.now();return i.schedule(U_,r,{index:0,period:n,subscriber:t})})}(e,t),function(e){return e.lift(new j_(i))};var i}function z_(e){return t=>t.lift(new W_(e))}class W_{constructor(e){this.notifier=e}call(e,t){const i=new $_(e),n=P(i,this.notifier);return n&&!i.seenValue?(i.add(n),t.subscribe(i)):i}}class $_ extends I{constructor(e){super(e),this.seenValue=!1}notifyNext(e,t,i,n,r){this.seenValue=!0,this.complete()}notifyComplete(){}}function K_(...e){const t=e[e.length-1];return x(t)?(e.pop(),i=>gp(e,i,t)):t=>gp(e,t)}class G_ extends Ip{constructor(e,t){super(e,t),this.scheduler=e,this.work=t}schedule(e,t=0){return t>0?super.schedule(e,t):(this.delay=t,this.state=e,this.scheduler.flush(this),this)}execute(e,t){return t>0||this.closed?super.execute(e,t):this._execute(e,t)}requestAsyncId(e,t,i=0){return null!==i&&i>0||null===i&&this.delay>0?super.requestAsyncId(e,t,i):e.flush(this)}}class Z_ extends Fp{}const Y_=new Z_(G_);class X_ extends p{constructor(e,t,i=0){super(e),this.scheduler=t,this.delay=i}static dispatch(e){const{notification:t,destination:i}=e;t.observe(i),this.unsubscribe()}scheduleMessage(e){this.destination.add(this.scheduler.schedule(X_.dispatch,this.delay,new Q_(e,this.destination)))}_next(e){this.scheduleMessage(Bp.createNext(e))}_error(e){this.scheduleMessage(Bp.createError(e)),this.unsubscribe()}_complete(){this.scheduleMessage(Bp.createComplete()),this.unsubscribe()}}class Q_{constructor(e,t){this.notification=e,this.destination=t}}class J_ extends S{constructor(e=Number.POSITIVE_INFINITY,t=Number.POSITIVE_INFINITY,i){super(),this.scheduler=i,this._events=[],this._infiniteTimeWindow=!1,this._bufferSize=e<1?1:e,this._windowTime=t<1?1:t,t===Number.POSITIVE_INFINITY?(this._infiniteTimeWindow=!0,this.next=this.nextInfiniteTimeWindow):this.next=this.nextTimeWindow}nextInfiniteTimeWindow(e){const t=this._events;t.push(e),t.length>this._bufferSize&&t.shift(),super.next(e)}nextTimeWindow(e){this._events.push(new em(this._getNow(),e)),this._trimBufferThenGetEvents(),super.next(e)}_subscribe(e){const t=this._infiniteTimeWindow,i=t?this._events:this._trimBufferThenGetEvents(),n=this.scheduler,r=i.length;let s;if(this.closed)throw new b;if(this.isStopped||this.hasError?s=u.EMPTY:(this.observers.push(e),s=new C(this,e)),n&&e.add(e=new X_(e,n)),t)for(let o=0;ot&&(s=Math.max(s,r-t)),s>0&&n.splice(0,s),n}}class em{constructor(e,t){this.time=e,this.value=t}}function tm(e,t,i){let n;return n=e&&"object"==typeof e?e:{bufferSize:e,windowTime:t,refCount:!1,scheduler:i},e=>e.lift(function({bufferSize:e=Number.POSITIVE_INFINITY,windowTime:t=Number.POSITIVE_INFINITY,refCount:i,scheduler:n}){let r,s,o=0,a=!1,l=!1;return function(c){o++,r&&!a||(a=!1,r=new J_(e,t,n),s=c.subscribe({next(e){r.next(e)},error(e){a=!0,r.error(e)},complete(){l=!0,s=void 0,r.complete()}}));const h=r.subscribe(this);this.add(()=>{o--,h.unsubscribe(),s&&!l&&i&&0===o&&(s.unsubscribe(),s=void 0,r=void 0)})}}(n))}let im;try{im="undefined"!=typeof Intl&&Intl.v8BreakIterator}catch(Bx){im=!1}let nm,rm=(()=>{class e{constructor(e){this._platformId=e,this.isBrowser=this._platformId?$h(this._platformId):"object"==typeof document&&!!document,this.EDGE=this.isBrowser&&/(edge)/i.test(navigator.userAgent),this.TRIDENT=this.isBrowser&&/(msie|trident)/i.test(navigator.userAgent),this.BLINK=this.isBrowser&&!(!window.chrome&&!im)&&"undefined"!=typeof CSS&&!this.EDGE&&!this.TRIDENT,this.WEBKIT=this.isBrowser&&/AppleWebKit/i.test(navigator.userAgent)&&!this.BLINK&&!this.EDGE&&!this.TRIDENT,this.IOS=this.isBrowser&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!("MSStream"in window),this.FIREFOX=this.isBrowser&&/(firefox|minefield)/i.test(navigator.userAgent),this.ANDROID=this.isBrowser&&/android/i.test(navigator.userAgent)&&!this.TRIDENT,this.SAFARI=this.isBrowser&&/safari/i.test(navigator.userAgent)&&this.WEBKIT}}return e.\u0275fac=function(t){return new(t||e)(mn(fc))},e.\u0275prov=pe({factory:function(){return new e(mn(fc))},token:e,providedIn:"root"}),e})(),sm=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})();const om=["color","button","checkbox","date","datetime-local","email","file","hidden","image","month","number","password","radio","range","reset","search","submit","tel","text","time","url","week"];function am(){if(nm)return nm;if("object"!=typeof document||!document)return nm=new Set(om),nm;let e=document.createElement("input");return nm=new Set(om.filter(t=>(e.setAttribute("type",t),e.type===t))),nm}let lm,cm,hm;function um(e){return function(){if(null==lm&&"undefined"!=typeof window)try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:()=>lm=!0}))}finally{lm=lm||!1}return lm}()?e:!!e.capture}function dm(){if(null==cm){if("object"!=typeof document||!document||"function"!=typeof Element||!Element)return cm=!1,cm;if("scrollBehavior"in document.documentElement.style)cm=!0;else{const e=Element.prototype.scrollTo;cm=!!e&&!/\{\s*\[native code\]\s*\}/.test(e.toString())}}return cm}function fm(e){if(function(){if(null==hm){const e="undefined"!=typeof document?document.head:null;hm=!(!e||!e.createShadowRoot&&!e.attachShadow)}return hm}()){const t=e.getRootNode?e.getRootNode():null;if("undefined"!=typeof ShadowRoot&&ShadowRoot&&t instanceof ShadowRoot)return t}return null}const pm=new Qi("cdk-dir-doc",{providedIn:"root",factory:function(){return gn(ah)}});let _m=(()=>{class e{constructor(e){if(this.value="ltr",this.change=new Ul,e){const t=e.documentElement?e.documentElement.dir:null,i=(e.body?e.body.dir:null)||t;this.value="ltr"===i||"rtl"===i?i:"ltr"}}ngOnDestroy(){this.change.complete()}}return e.\u0275fac=function(t){return new(t||e)(mn(pm,8))},e.\u0275prov=pe({factory:function(){return new e(mn(pm,8))},token:e,providedIn:"root"}),e})(),mm=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})();class gm{constructor(e=!1,t,i=!0){this._multiple=e,this._emitChanges=i,this._selection=new Set,this._deselectedToEmit=[],this._selectedToEmit=[],this.changed=new S,t&&t.length&&(e?t.forEach(e=>this._markSelected(e)):this._markSelected(t[0]),this._selectedToEmit.length=0)}get selected(){return this._selected||(this._selected=Array.from(this._selection.values())),this._selected}select(...e){this._verifyValueAssignment(e),e.forEach(e=>this._markSelected(e)),this._emitChangeEvent()}deselect(...e){this._verifyValueAssignment(e),e.forEach(e=>this._unmarkSelected(e)),this._emitChangeEvent()}toggle(e){this.isSelected(e)?this.deselect(e):this.select(e)}clear(){this._unmarkAll(),this._emitChangeEvent()}isSelected(e){return this._selection.has(e)}isEmpty(){return 0===this._selection.size}hasValue(){return!this.isEmpty()}sort(e){this._multiple&&this.selected&&this._selected.sort(e)}isMultipleSelection(){return this._multiple}_emitChangeEvent(){this._selected=null,(this._selectedToEmit.length||this._deselectedToEmit.length)&&(this.changed.next({source:this,added:this._selectedToEmit,removed:this._deselectedToEmit}),this._deselectedToEmit=[],this._selectedToEmit=[])}_markSelected(e){this.isSelected(e)||(this._multiple||this._unmarkAll(),this._selection.add(e),this._emitChanges&&this._selectedToEmit.push(e))}_unmarkSelected(e){this.isSelected(e)&&(this._selection.delete(e),this._emitChanges&&this._deselectedToEmit.push(e))}_unmarkAll(){this.isEmpty()||this._selection.forEach(e=>this._unmarkSelected(e))}_verifyValueAssignment(e){}}let vm=(()=>{class e{constructor(e,t,i){this._ngZone=e,this._platform=t,this._scrolled=new S,this._globalSubscription=null,this._scrolledCount=0,this.scrollContainers=new Map,this._document=i}register(e){this.scrollContainers.has(e)||this.scrollContainers.set(e,e.elementScrolled().subscribe(()=>this._scrolled.next(e)))}deregister(e){const t=this.scrollContainers.get(e);t&&(t.unsubscribe(),this.scrollContainers.delete(e))}scrolled(e=20){return this._platform.isBrowser?new v(t=>{this._globalSubscription||this._addGlobalListener();const i=e>0?this._scrolled.pipe(q_(e)).subscribe(t):this._scrolled.subscribe(t);return this._scrolledCount++,()=>{i.unsubscribe(),this._scrolledCount--,this._scrolledCount||this._removeGlobalListener()}}):mp()}ngOnDestroy(){this._removeGlobalListener(),this.scrollContainers.forEach((e,t)=>this.deregister(t)),this._scrolled.complete()}ancestorScrolled(e,t){const i=this.getAncestorScrollContainers(e);return this.scrolled(t).pipe(bp(e=>!e||i.indexOf(e)>-1))}getAncestorScrollContainers(e){const t=[];return this.scrollContainers.forEach((i,n)=>{this._scrollableContainsElement(n,e)&&t.push(n)}),t}_getWindow(){return this._document.defaultView||window}_scrollableContainsElement(e,t){let i=F_(t),n=e.getElementRef().nativeElement;do{if(i==n)return!0}while(i=i.parentElement);return!1}_addGlobalListener(){this._globalSubscription=this._ngZone.runOutsideAngular(()=>_p(this._getWindow().document,"scroll").subscribe(()=>this._scrolled.next()))}_removeGlobalListener(){this._globalSubscription&&(this._globalSubscription.unsubscribe(),this._globalSubscription=null)}}return e.\u0275fac=function(t){return new(t||e)(mn(Tc),mn(rm),mn(ah,8))},e.\u0275prov=pe({factory:function(){return new e(mn(Tc),mn(rm),mn(ah,8))},token:e,providedIn:"root"}),e})(),ym=(()=>{class e{constructor(e,t,i){this._platform=e,this._change=new S,this._changeListener=e=>{this._change.next(e)},this._document=i,t.runOutsideAngular(()=>{if(e.isBrowser){const e=this._getWindow();e.addEventListener("resize",this._changeListener),e.addEventListener("orientationchange",this._changeListener)}this.change().subscribe(()=>this._updateViewportSize())})}ngOnDestroy(){if(this._platform.isBrowser){const e=this._getWindow();e.removeEventListener("resize",this._changeListener),e.removeEventListener("orientationchange",this._changeListener)}this._change.complete()}getViewportSize(){this._viewportSize||this._updateViewportSize();const e={width:this._viewportSize.width,height:this._viewportSize.height};return this._platform.isBrowser||(this._viewportSize=null),e}getViewportRect(){const e=this.getViewportScrollPosition(),{width:t,height:i}=this.getViewportSize();return{top:e.top,left:e.left,bottom:e.top+i,right:e.left+t,height:i,width:t}}getViewportScrollPosition(){if(!this._platform.isBrowser)return{top:0,left:0};const e=this._document,t=this._getWindow(),i=e.documentElement,n=i.getBoundingClientRect();return{top:-n.top||e.body.scrollTop||t.scrollY||i.scrollTop||0,left:-n.left||e.body.scrollLeft||t.scrollX||i.scrollLeft||0}}change(e=20){return e>0?this._change.pipe(q_(e)):this._change}_getWindow(){return this._document.defaultView||window}_updateViewportSize(){const e=this._getWindow();this._viewportSize=this._platform.isBrowser?{width:e.innerWidth,height:e.innerHeight}:{width:0,height:0}}}return e.\u0275fac=function(t){return new(t||e)(mn(rm),mn(Tc),mn(ah,8))},e.\u0275prov=pe({factory:function(){return new e(mn(rm),mn(Tc),mn(ah,8))},token:e,providedIn:"root"}),e})(),bm=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})(),Cm=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[mm,sm,bm],mm,bm]}),e})();class wm{attach(e){return this._attachedHost=e,e.attach(this)}detach(){let e=this._attachedHost;null!=e&&(this._attachedHost=null,e.detach())}get isAttached(){return null!=this._attachedHost}setAttachedHost(e){this._attachedHost=e}}class Sm extends wm{constructor(e,t,i,n){super(),this.component=e,this.viewContainerRef=t,this.injector=i,this.componentFactoryResolver=n}}class km extends wm{constructor(e,t,i){super(),this.templateRef=e,this.viewContainerRef=t,this.context=i}get origin(){return this.templateRef.elementRef}attach(e,t=this.context){return this.context=t,super.attach(e)}detach(){return this.context=void 0,super.detach()}}class xm extends wm{constructor(e){super(),this.element=e instanceof ja?e.nativeElement:e}}class Em{constructor(){this._isDisposed=!1,this.attachDomPortal=null}hasAttached(){return!!this._attachedPortal}attach(e){return e instanceof Sm?(this._attachedPortal=e,this.attachComponentPortal(e)):e instanceof km?(this._attachedPortal=e,this.attachTemplatePortal(e)):this.attachDomPortal&&e instanceof xm?(this._attachedPortal=e,this.attachDomPortal(e)):void 0}detach(){this._attachedPortal&&(this._attachedPortal.setAttachedHost(null),this._attachedPortal=null),this._invokeDisposeFn()}dispose(){this.hasAttached()&&this.detach(),this._invokeDisposeFn(),this._isDisposed=!0}setDisposeFn(e){this._disposeFn=e}_invokeDisposeFn(){this._disposeFn&&(this._disposeFn(),this._disposeFn=null)}}class Am extends Em{constructor(e,t,i,n,r){super(),this.outletElement=e,this._componentFactoryResolver=t,this._appRef=i,this._defaultInjector=n,this.attachDomPortal=e=>{const t=e.element,i=this._document.createComment("dom-portal");t.parentNode.insertBefore(i,t),this.outletElement.appendChild(t),super.setDisposeFn(()=>{i.parentNode&&i.parentNode.replaceChild(t,i)})},this._document=r}attachComponentPortal(e){const t=(e.componentFactoryResolver||this._componentFactoryResolver).resolveComponentFactory(e.component);let i;return e.viewContainerRef?(i=e.viewContainerRef.createComponent(t,e.viewContainerRef.length,e.injector||e.viewContainerRef.injector),this.setDisposeFn(()=>i.destroy())):(i=t.create(e.injector||this._defaultInjector),this._appRef.attachView(i.hostView),this.setDisposeFn(()=>{this._appRef.detachView(i.hostView),i.destroy()})),this.outletElement.appendChild(this._getComponentRootNode(i)),i}attachTemplatePortal(e){let t=e.viewContainerRef,i=t.createEmbeddedView(e.templateRef,e.context);return i.rootNodes.forEach(e=>this.outletElement.appendChild(e)),i.detectChanges(),this.setDisposeFn(()=>{let e=t.indexOf(i);-1!==e&&t.remove(e)}),i}dispose(){super.dispose(),null!=this.outletElement.parentNode&&this.outletElement.parentNode.removeChild(this.outletElement)}_getComponentRootNode(e){return e.hostView.rootNodes[0]}}let Tm=(()=>{class e extends Em{constructor(e,t,i){super(),this._componentFactoryResolver=e,this._viewContainerRef=t,this._isInitialized=!1,this.attached=new Ul,this.attachDomPortal=e=>{const t=e.element,i=this._document.createComment("dom-portal");e.setAttachedHost(this),t.parentNode.insertBefore(i,t),this._getRootNode().appendChild(t),super.setDisposeFn(()=>{i.parentNode&&i.parentNode.replaceChild(t,i)})},this._document=i}get portal(){return this._attachedPortal}set portal(e){(!this.hasAttached()||e||this._isInitialized)&&(this.hasAttached()&&super.detach(),e&&super.attach(e),this._attachedPortal=e)}get attachedRef(){return this._attachedRef}ngOnInit(){this._isInitialized=!0}ngOnDestroy(){super.dispose(),this._attachedPortal=null,this._attachedRef=null}attachComponentPortal(e){e.setAttachedHost(this);const t=null!=e.viewContainerRef?e.viewContainerRef:this._viewContainerRef,i=(e.componentFactoryResolver||this._componentFactoryResolver).resolveComponentFactory(e.component),n=t.createComponent(i,t.length,e.injector||t.injector);return t!==this._viewContainerRef&&this._getRootNode().appendChild(n.hostView.rootNodes[0]),super.setDisposeFn(()=>n.destroy()),this._attachedPortal=e,this._attachedRef=n,this.attached.emit(n),n}attachTemplatePortal(e){e.setAttachedHost(this);const t=this._viewContainerRef.createEmbeddedView(e.templateRef,e.context);return super.setDisposeFn(()=>this._viewContainerRef.clear()),this._attachedPortal=e,this._attachedRef=t,this.attached.emit(t),t}_getRootNode(){const e=this._viewContainerRef.element.nativeElement;return e.nodeType===e.ELEMENT_NODE?e:e.parentNode}}return e.\u0275fac=function(t){return new(t||e)(xo(Ma),xo(Sl),xo(ah))},e.\u0275dir=Qe({type:e,selectors:[["","cdkPortalOutlet",""]],inputs:{portal:["cdkPortalOutlet","portal"]},outputs:{attached:"attached"},exportAs:["cdkPortalOutlet"],features:[lo]}),e})(),Om=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})();class Rm{constructor(e,t){this.predicate=e,this.inclusive=t}call(e,t){return t.subscribe(new Lm(e,this.predicate,this.inclusive))}}class Lm extends p{constructor(e,t,i){super(e),this.predicate=t,this.inclusive=i,this.index=0}_next(e){const t=this.destination;let i;try{i=this.predicate(e,this.index++)}catch(n){return void t.error(n)}this.nextOrComplete(e,i)}nextOrComplete(e,t){const i=this.destination;Boolean(t)?i.next(e):(this.inclusive&&i.next(e),i.complete())}}function Dm(e,...t){return t.length?t.some(t=>e[t]):e.altKey||e.shiftKey||e.ctrlKey||e.metaKey}const Pm=dm();class Im{constructor(e,t){this._viewportRuler=e,this._previousHTMLStyles={top:"",left:""},this._isEnabled=!1,this._document=t}attach(){}enable(){if(this._canBeEnabled()){const e=this._document.documentElement;this._previousScrollPosition=this._viewportRuler.getViewportScrollPosition(),this._previousHTMLStyles.left=e.style.left||"",this._previousHTMLStyles.top=e.style.top||"",e.style.left=M_(-this._previousScrollPosition.left),e.style.top=M_(-this._previousScrollPosition.top),e.classList.add("cdk-global-scrollblock"),this._isEnabled=!0}}disable(){if(this._isEnabled){const e=this._document.documentElement,t=e.style,i=this._document.body.style,n=t.scrollBehavior||"",r=i.scrollBehavior||"";this._isEnabled=!1,t.left=this._previousHTMLStyles.left,t.top=this._previousHTMLStyles.top,e.classList.remove("cdk-global-scrollblock"),Pm&&(t.scrollBehavior=i.scrollBehavior="auto"),window.scroll(this._previousScrollPosition.left,this._previousScrollPosition.top),Pm&&(t.scrollBehavior=n,i.scrollBehavior=r)}}_canBeEnabled(){if(this._document.documentElement.classList.contains("cdk-global-scrollblock")||this._isEnabled)return!1;const e=this._document.body,t=this._viewportRuler.getViewportSize();return e.scrollHeight>t.height||e.scrollWidth>t.width}}class Mm{constructor(e,t,i,n){this._scrollDispatcher=e,this._ngZone=t,this._viewportRuler=i,this._config=n,this._scrollSubscription=null,this._detach=()=>{this.disable(),this._overlayRef.hasAttached()&&this._ngZone.run(()=>this._overlayRef.detach())}}attach(e){this._overlayRef=e}enable(){if(this._scrollSubscription)return;const e=this._scrollDispatcher.scrolled(0);this._config&&this._config.threshold&&this._config.threshold>1?(this._initialScrollPosition=this._viewportRuler.getViewportScrollPosition().top,this._scrollSubscription=e.subscribe(()=>{const e=this._viewportRuler.getViewportScrollPosition().top;Math.abs(e-this._initialScrollPosition)>this._config.threshold?this._detach():this._overlayRef.updatePosition()})):this._scrollSubscription=e.subscribe(this._detach)}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}}class Fm{enable(){}disable(){}attach(){}}function Hm(e,t){return t.some(t=>e.bottomt.bottom||e.rightt.right)}function Bm(e,t){return t.some(t=>e.topt.bottom||e.leftt.right)}class jm{constructor(e,t,i,n){this._scrollDispatcher=e,this._viewportRuler=t,this._ngZone=i,this._config=n,this._scrollSubscription=null}attach(e){this._overlayRef=e}enable(){this._scrollSubscription||(this._scrollSubscription=this._scrollDispatcher.scrolled(this._config?this._config.scrollThrottle:0).subscribe(()=>{if(this._overlayRef.updatePosition(),this._config&&this._config.autoClose){const e=this._overlayRef.overlayElement.getBoundingClientRect(),{width:t,height:i}=this._viewportRuler.getViewportSize();Hm(e,[{width:t,height:i,bottom:i,right:t,top:0,left:0}])&&(this.disable(),this._ngZone.run(()=>this._overlayRef.detach()))}}))}disable(){this._scrollSubscription&&(this._scrollSubscription.unsubscribe(),this._scrollSubscription=null)}detach(){this.disable(),this._overlayRef=null}}let Nm=(()=>{class e{constructor(e,t,i,n){this._scrollDispatcher=e,this._viewportRuler=t,this._ngZone=i,this.noop=()=>new Fm,this.close=e=>new Mm(this._scrollDispatcher,this._ngZone,this._viewportRuler,e),this.block=()=>new Im(this._viewportRuler,this._document),this.reposition=e=>new jm(this._scrollDispatcher,this._viewportRuler,this._ngZone,e),this._document=n}}return e.\u0275fac=function(t){return new(t||e)(mn(vm),mn(ym),mn(Tc),mn(ah))},e.\u0275prov=pe({factory:function(){return new e(mn(vm),mn(ym),mn(Tc),mn(ah))},token:e,providedIn:"root"}),e})();class Vm{constructor(e){if(this.scrollStrategy=new Fm,this.panelClass="",this.hasBackdrop=!1,this.backdropClass="cdk-overlay-dark-backdrop",this.disposeOnNavigation=!1,e){const t=Object.keys(e);for(const i of t)void 0!==e[i]&&(this[i]=e[i])}}}class Um{constructor(e,t,i,n,r){this.offsetX=i,this.offsetY=n,this.panelClass=r,this.originX=e.originX,this.originY=e.originY,this.overlayX=t.overlayX,this.overlayY=t.overlayY}}class qm{constructor(e,t){this.connectionPair=e,this.scrollableViewProperties=t}}let zm=(()=>{class e{constructor(e){this._attachedOverlays=[],this._document=e}ngOnDestroy(){this.detach()}add(e){this.remove(e),this._attachedOverlays.push(e)}remove(e){const t=this._attachedOverlays.indexOf(e);t>-1&&this._attachedOverlays.splice(t,1),0===this._attachedOverlays.length&&this.detach()}}return e.\u0275fac=function(t){return new(t||e)(mn(ah))},e.\u0275prov=pe({factory:function(){return new e(mn(ah))},token:e,providedIn:"root"}),e})(),Wm=(()=>{class e extends zm{constructor(e){super(e),this._keydownListener=e=>{const t=this._attachedOverlays;for(let i=t.length-1;i>-1;i--)if(t[i]._keydownEvents.observers.length>0){t[i]._keydownEvents.next(e);break}}}add(e){super.add(e),this._isAttached||(this._document.body.addEventListener("keydown",this._keydownListener),this._isAttached=!0)}detach(){this._isAttached&&(this._document.body.removeEventListener("keydown",this._keydownListener),this._isAttached=!1)}}return e.\u0275fac=function(t){return new(t||e)(mn(ah))},e.\u0275prov=pe({factory:function(){return new e(mn(ah))},token:e,providedIn:"root"}),e})(),$m=(()=>{class e extends zm{constructor(e,t){super(e),this._platform=t,this._cursorStyleIsSet=!1,this._clickListener=e=>{const t=e.composedPath?e.composedPath()[0]:e.target,i=this._attachedOverlays.slice();for(let n=i.length-1;n>-1;n--){const r=i[n];if(!(r._outsidePointerEvents.observers.length<1)&&r.hasAttached()){if(r.overlayElement.contains(t))break;r._outsidePointerEvents.next(e)}}}}add(e){super.add(e),this._isAttached||(this._document.body.addEventListener("click",this._clickListener,!0),this._document.body.addEventListener("contextmenu",this._clickListener,!0),this._platform.IOS&&!this._cursorStyleIsSet&&(this._cursorOriginalValue=this._document.body.style.cursor,this._document.body.style.cursor="pointer",this._cursorStyleIsSet=!0),this._isAttached=!0)}detach(){this._isAttached&&(this._document.body.removeEventListener("click",this._clickListener,!0),this._document.body.removeEventListener("contextmenu",this._clickListener,!0),this._platform.IOS&&this._cursorStyleIsSet&&(this._document.body.style.cursor=this._cursorOriginalValue,this._cursorStyleIsSet=!1),this._isAttached=!1)}}return e.\u0275fac=function(t){return new(t||e)(mn(ah),mn(rm))},e.\u0275prov=pe({factory:function(){return new e(mn(ah),mn(rm))},token:e,providedIn:"root"}),e})();const Km=!("undefined"==typeof window||!window||!window.__karma__&&!window.jasmine);let Gm=(()=>{class e{constructor(e,t){this._platform=t,this._document=e}ngOnDestroy(){const e=this._containerElement;e&&e.parentNode&&e.parentNode.removeChild(e)}getContainerElement(){return this._containerElement||this._createContainer(),this._containerElement}_createContainer(){const e="cdk-overlay-container";if(this._platform.isBrowser||Km){const t=this._document.querySelectorAll(`.${e}[platform="server"], .${e}[platform="test"]`);for(let e=0;ethis._backdropClick.next(e),this._keydownEvents=new S,this._outsidePointerEvents=new S,n.scrollStrategy&&(this._scrollStrategy=n.scrollStrategy,this._scrollStrategy.attach(this)),this._positionStrategy=n.positionStrategy}get overlayElement(){return this._pane}get backdropElement(){return this._backdropElement}get hostElement(){return this._host}attach(e){let t=this._portalOutlet.attach(e);return!this._host.parentElement&&this._previousHostParent&&this._previousHostParent.appendChild(this._host),this._positionStrategy&&this._positionStrategy.attach(this),this._updateStackingOrder(),this._updateElementSize(),this._updateElementDirection(),this._scrollStrategy&&this._scrollStrategy.enable(),this._ngZone.onStable.pipe(Ap(1)).subscribe(()=>{this.hasAttached()&&this.updatePosition()}),this._togglePointerEvents(!0),this._config.hasBackdrop&&this._attachBackdrop(),this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!0),this._attachments.next(),this._keyboardDispatcher.add(this),this._config.disposeOnNavigation&&(this._locationChanges=this._location.subscribe(()=>this.dispose())),this._outsideClickDispatcher.add(this),t}detach(){if(!this.hasAttached())return;this.detachBackdrop(),this._togglePointerEvents(!1),this._positionStrategy&&this._positionStrategy.detach&&this._positionStrategy.detach(),this._scrollStrategy&&this._scrollStrategy.disable();const e=this._portalOutlet.detach();return this._detachments.next(),this._keyboardDispatcher.remove(this),this._detachContentWhenStable(),this._locationChanges.unsubscribe(),this._outsideClickDispatcher.remove(this),e}dispose(){const e=this.hasAttached();this._positionStrategy&&this._positionStrategy.dispose(),this._disposeScrollStrategy(),this.detachBackdrop(),this._locationChanges.unsubscribe(),this._keyboardDispatcher.remove(this),this._portalOutlet.dispose(),this._attachments.complete(),this._backdropClick.complete(),this._keydownEvents.complete(),this._outsidePointerEvents.complete(),this._outsideClickDispatcher.remove(this),this._host&&this._host.parentNode&&(this._host.parentNode.removeChild(this._host),this._host=null),this._previousHostParent=this._pane=null,e&&this._detachments.next(),this._detachments.complete()}hasAttached(){return this._portalOutlet.hasAttached()}backdropClick(){return this._backdropClick}attachments(){return this._attachments}detachments(){return this._detachments}keydownEvents(){return this._keydownEvents}outsidePointerEvents(){return this._outsidePointerEvents}getConfig(){return this._config}updatePosition(){this._positionStrategy&&this._positionStrategy.apply()}updatePositionStrategy(e){e!==this._positionStrategy&&(this._positionStrategy&&this._positionStrategy.dispose(),this._positionStrategy=e,this.hasAttached()&&(e.attach(this),this.updatePosition()))}updateSize(e){this._config=Object.assign(Object.assign({},this._config),e),this._updateElementSize()}setDirection(e){this._config=Object.assign(Object.assign({},this._config),{direction:e}),this._updateElementDirection()}addPanelClass(e){this._pane&&this._toggleClasses(this._pane,e,!0)}removePanelClass(e){this._pane&&this._toggleClasses(this._pane,e,!1)}getDirection(){const e=this._config.direction;return e?"string"==typeof e?e:e.value:"ltr"}updateScrollStrategy(e){e!==this._scrollStrategy&&(this._disposeScrollStrategy(),this._scrollStrategy=e,this.hasAttached()&&(e.attach(this),e.enable()))}_updateElementDirection(){this._host.setAttribute("dir",this.getDirection())}_updateElementSize(){if(!this._pane)return;const e=this._pane.style;e.width=M_(this._config.width),e.height=M_(this._config.height),e.minWidth=M_(this._config.minWidth),e.minHeight=M_(this._config.minHeight),e.maxWidth=M_(this._config.maxWidth),e.maxHeight=M_(this._config.maxHeight)}_togglePointerEvents(e){this._pane.style.pointerEvents=e?"":"none"}_attachBackdrop(){const e="cdk-overlay-backdrop-showing";this._backdropElement=this._document.createElement("div"),this._backdropElement.classList.add("cdk-overlay-backdrop"),this._config.backdropClass&&this._toggleClasses(this._backdropElement,this._config.backdropClass,!0),this._host.parentElement.insertBefore(this._backdropElement,this._host),this._backdropElement.addEventListener("click",this._backdropClickHandler),"undefined"!=typeof requestAnimationFrame?this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>{this._backdropElement&&this._backdropElement.classList.add(e)})}):this._backdropElement.classList.add(e)}_updateStackingOrder(){this._host.nextSibling&&this._host.parentNode.appendChild(this._host)}detachBackdrop(){let e,t=this._backdropElement;if(!t)return;let i=()=>{t&&(t.removeEventListener("click",this._backdropClickHandler),t.removeEventListener("transitionend",i),t.parentNode&&t.parentNode.removeChild(t)),this._backdropElement==t&&(this._backdropElement=null),this._config.backdropClass&&this._toggleClasses(t,this._config.backdropClass,!1),clearTimeout(e)};t.classList.remove("cdk-overlay-backdrop-showing"),this._ngZone.runOutsideAngular(()=>{t.addEventListener("transitionend",i)}),t.style.pointerEvents="none",e=this._ngZone.runOutsideAngular(()=>setTimeout(i,500))}_toggleClasses(e,t,i){const n=e.classList;I_(t).forEach(e=>{e&&(i?n.add(e):n.remove(e))})}_detachContentWhenStable(){this._ngZone.runOutsideAngular(()=>{const e=this._ngZone.onStable.pipe(z_(W(this._attachments,this._detachments))).subscribe(()=>{this._pane&&this._host&&0!==this._pane.children.length||(this._pane&&this._config.panelClass&&this._toggleClasses(this._pane,this._config.panelClass,!1),this._host&&this._host.parentElement&&(this._previousHostParent=this._host.parentElement,this._previousHostParent.removeChild(this._host)),e.unsubscribe())})})}_disposeScrollStrategy(){const e=this._scrollStrategy;e&&(e.disable(),e.detach&&e.detach())}}const Ym="cdk-overlay-connected-position-bounding-box",Xm=/([A-Za-z%]+)$/;class Qm{constructor(e,t,i,n,r){this._viewportRuler=t,this._document=i,this._platform=n,this._overlayContainer=r,this._lastBoundingBoxSize={width:0,height:0},this._isPushed=!1,this._canPush=!0,this._growAfterOpen=!1,this._hasFlexibleDimensions=!0,this._positionLocked=!1,this._viewportMargin=0,this._scrollables=[],this._preferredPositions=[],this._positionChanges=new S,this._resizeSubscription=u.EMPTY,this._offsetX=0,this._offsetY=0,this._appliedPanelClasses=[],this.positionChanges=this._positionChanges,this.setOrigin(e)}get positions(){return this._preferredPositions}attach(e){this._validatePositions(),e.hostElement.classList.add(Ym),this._overlayRef=e,this._boundingBox=e.hostElement,this._pane=e.overlayElement,this._isDisposed=!1,this._isInitialRender=!0,this._lastPosition=null,this._resizeSubscription.unsubscribe(),this._resizeSubscription=this._viewportRuler.change().subscribe(()=>{this._isInitialRender=!0,this.apply()})}apply(){if(this._isDisposed||!this._platform.isBrowser)return;if(!this._isInitialRender&&this._positionLocked&&this._lastPosition)return void this.reapplyLastPosition();this._clearPanelClasses(),this._resetOverlayElementStyles(),this._resetBoundingBoxStyles(),this._viewportRect=this._getNarrowedViewportRect(),this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect();const e=this._originRect,t=this._overlayRect,i=this._viewportRect,n=[];let r;for(let s of this._preferredPositions){let o=this._getOriginPoint(e,s),a=this._getOverlayPoint(o,t,s),l=this._getOverlayFit(a,t,i,s);if(l.isCompletelyWithinViewport)return this._isPushed=!1,void this._applyPosition(s,o);this._canFitWithFlexibleDimensions(l,a,i)?n.push({position:s,origin:o,overlayRect:t,boundingBoxRect:this._calculateBoundingBoxRect(o,s)}):(!r||r.overlayFit.visibleAreat&&(t=n,e=i)}return this._isPushed=!1,void this._applyPosition(e.position,e.origin)}if(this._canPush)return this._isPushed=!0,void this._applyPosition(r.position,r.originPoint);this._applyPosition(r.position,r.originPoint)}detach(){this._clearPanelClasses(),this._lastPosition=null,this._previousPushAmount=null,this._resizeSubscription.unsubscribe()}dispose(){this._isDisposed||(this._boundingBox&&Jm(this._boundingBox.style,{top:"",left:"",right:"",bottom:"",height:"",width:"",alignItems:"",justifyContent:""}),this._pane&&this._resetOverlayElementStyles(),this._overlayRef&&this._overlayRef.hostElement.classList.remove(Ym),this.detach(),this._positionChanges.complete(),this._overlayRef=this._boundingBox=null,this._isDisposed=!0)}reapplyLastPosition(){if(!this._isDisposed&&(!this._platform||this._platform.isBrowser)){this._originRect=this._getOriginRect(),this._overlayRect=this._pane.getBoundingClientRect(),this._viewportRect=this._getNarrowedViewportRect();const e=this._lastPosition||this._preferredPositions[0],t=this._getOriginPoint(this._originRect,e);this._applyPosition(e,t)}}withScrollableContainers(e){return this._scrollables=e,this}withPositions(e){return this._preferredPositions=e,-1===e.indexOf(this._lastPosition)&&(this._lastPosition=null),this._validatePositions(),this}withViewportMargin(e){return this._viewportMargin=e,this}withFlexibleDimensions(e=!0){return this._hasFlexibleDimensions=e,this}withGrowAfterOpen(e=!0){return this._growAfterOpen=e,this}withPush(e=!0){return this._canPush=e,this}withLockedPosition(e=!0){return this._positionLocked=e,this}setOrigin(e){return this._origin=e,this}withDefaultOffsetX(e){return this._offsetX=e,this}withDefaultOffsetY(e){return this._offsetY=e,this}withTransformOriginOn(e){return this._transformOriginSelector=e,this}_getOriginPoint(e,t){let i,n;if("center"==t.originX)i=e.left+e.width/2;else{const n=this._isRtl()?e.right:e.left,r=this._isRtl()?e.left:e.right;i="start"==t.originX?n:r}return n="center"==t.originY?e.top+e.height/2:"top"==t.originY?e.top:e.bottom,{x:i,y:n}}_getOverlayPoint(e,t,i){let n,r;return n="center"==i.overlayX?-t.width/2:"start"===i.overlayX?this._isRtl()?-t.width:0:this._isRtl()?0:-t.width,r="center"==i.overlayY?-t.height/2:"top"==i.overlayY?0:-t.height,{x:e.x+n,y:e.y+r}}_getOverlayFit(e,t,i,n){const r=tg(t);let{x:s,y:o}=e,a=this._getOffset(n,"x"),l=this._getOffset(n,"y");a&&(s+=a),l&&(o+=l);let c=0-o,h=o+r.height-i.height,u=this._subtractOverflows(r.width,0-s,s+r.width-i.width),d=this._subtractOverflows(r.height,c,h),f=u*d;return{visibleArea:f,isCompletelyWithinViewport:r.width*r.height===f,fitsInViewportVertically:d===r.height,fitsInViewportHorizontally:u==r.width}}_canFitWithFlexibleDimensions(e,t,i){if(this._hasFlexibleDimensions){const n=i.bottom-t.y,r=i.right-t.x,s=eg(this._overlayRef.getConfig().minHeight),o=eg(this._overlayRef.getConfig().minWidth),a=e.fitsInViewportHorizontally||null!=o&&o<=r;return(e.fitsInViewportVertically||null!=s&&s<=n)&&a}return!1}_pushOverlayOnScreen(e,t,i){if(this._previousPushAmount&&this._positionLocked)return{x:e.x+this._previousPushAmount.x,y:e.y+this._previousPushAmount.y};const n=tg(t),r=this._viewportRect,s=Math.max(e.x+n.width-r.width,0),o=Math.max(e.y+n.height-r.height,0),a=Math.max(r.top-i.top-e.y,0),l=Math.max(r.left-i.left-e.x,0);let c=0,h=0;return c=n.width<=r.width?l||-s:e.xn&&!this._isInitialRender&&!this._growAfterOpen&&(s=e.y-n/2)}if("end"===t.overlayX&&!n||"start"===t.overlayX&&n)c=i.width-e.x+this._viewportMargin,a=e.x-this._viewportMargin;else if("start"===t.overlayX&&!n||"end"===t.overlayX&&n)l=e.x,a=i.right-e.x;else{const t=Math.min(i.right-e.x+i.left,e.x),n=this._lastBoundingBoxSize.width;a=2*t,l=e.x-t,a>n&&!this._isInitialRender&&!this._growAfterOpen&&(l=e.x-n/2)}return{top:s,left:l,bottom:o,right:c,width:a,height:r}}_setBoundingBoxStyles(e,t){const i=this._calculateBoundingBoxRect(e,t);this._isInitialRender||this._growAfterOpen||(i.height=Math.min(i.height,this._lastBoundingBoxSize.height),i.width=Math.min(i.width,this._lastBoundingBoxSize.width));const n={};if(this._hasExactPosition())n.top=n.left="0",n.bottom=n.right=n.maxHeight=n.maxWidth="",n.width=n.height="100%";else{const e=this._overlayRef.getConfig().maxHeight,r=this._overlayRef.getConfig().maxWidth;n.height=M_(i.height),n.top=M_(i.top),n.bottom=M_(i.bottom),n.width=M_(i.width),n.left=M_(i.left),n.right=M_(i.right),n.alignItems="center"===t.overlayX?"center":"end"===t.overlayX?"flex-end":"flex-start",n.justifyContent="center"===t.overlayY?"center":"bottom"===t.overlayY?"flex-end":"flex-start",e&&(n.maxHeight=M_(e)),r&&(n.maxWidth=M_(r))}this._lastBoundingBoxSize=i,Jm(this._boundingBox.style,n)}_resetBoundingBoxStyles(){Jm(this._boundingBox.style,{top:"0",left:"0",right:"0",bottom:"0",height:"",width:"",alignItems:"",justifyContent:""})}_resetOverlayElementStyles(){Jm(this._pane.style,{top:"",left:"",bottom:"",right:"",position:"",transform:""})}_setOverlayElementStyles(e,t){const i={},n=this._hasExactPosition(),r=this._hasFlexibleDimensions,s=this._overlayRef.getConfig();if(n){const n=this._viewportRuler.getViewportScrollPosition();Jm(i,this._getExactOverlayY(t,e,n)),Jm(i,this._getExactOverlayX(t,e,n))}else i.position="static";let o="",a=this._getOffset(t,"x"),l=this._getOffset(t,"y");a&&(o+=`translateX(${a}px) `),l&&(o+=`translateY(${l}px)`),i.transform=o.trim(),s.maxHeight&&(n?i.maxHeight=M_(s.maxHeight):r&&(i.maxHeight="")),s.maxWidth&&(n?i.maxWidth=M_(s.maxWidth):r&&(i.maxWidth="")),Jm(this._pane.style,i)}_getExactOverlayY(e,t,i){let n={top:"",bottom:""},r=this._getOverlayPoint(t,this._overlayRect,e);this._isPushed&&(r=this._pushOverlayOnScreen(r,this._overlayRect,i));let s=this._overlayContainer.getContainerElement().getBoundingClientRect().top;return r.y-=s,"bottom"===e.overlayY?n.bottom=this._document.documentElement.clientHeight-(r.y+this._overlayRect.height)+"px":n.top=M_(r.y),n}_getExactOverlayX(e,t,i){let n,r={left:"",right:""},s=this._getOverlayPoint(t,this._overlayRect,e);return this._isPushed&&(s=this._pushOverlayOnScreen(s,this._overlayRect,i)),n=this._isRtl()?"end"===e.overlayX?"left":"right":"end"===e.overlayX?"right":"left","right"===n?r.right=this._document.documentElement.clientWidth-(s.x+this._overlayRect.width)+"px":r.left=M_(s.x),r}_getScrollVisibility(){const e=this._getOriginRect(),t=this._pane.getBoundingClientRect(),i=this._scrollables.map(e=>e.getElementRef().nativeElement.getBoundingClientRect());return{isOriginClipped:Bm(e,i),isOriginOutsideView:Hm(e,i),isOverlayClipped:Bm(t,i),isOverlayOutsideView:Hm(t,i)}}_subtractOverflows(e,...t){return t.reduce((e,t)=>e-Math.max(t,0),e)}_getNarrowedViewportRect(){const e=this._document.documentElement.clientWidth,t=this._document.documentElement.clientHeight,i=this._viewportRuler.getViewportScrollPosition();return{top:i.top+this._viewportMargin,left:i.left+this._viewportMargin,right:i.left+e-this._viewportMargin,bottom:i.top+t-this._viewportMargin,width:e-2*this._viewportMargin,height:t-2*this._viewportMargin}}_isRtl(){return"rtl"===this._overlayRef.getDirection()}_hasExactPosition(){return!this._hasFlexibleDimensions||this._isPushed}_getOffset(e,t){return"x"===t?null==e.offsetX?this._offsetX:e.offsetX:null==e.offsetY?this._offsetY:e.offsetY}_validatePositions(){}_addPanelClasses(e){this._pane&&I_(e).forEach(e=>{""!==e&&-1===this._appliedPanelClasses.indexOf(e)&&(this._appliedPanelClasses.push(e),this._pane.classList.add(e))})}_clearPanelClasses(){this._pane&&(this._appliedPanelClasses.forEach(e=>{this._pane.classList.remove(e)}),this._appliedPanelClasses=[])}_getOriginRect(){const e=this._origin;if(e instanceof ja)return e.nativeElement.getBoundingClientRect();if(e instanceof Element)return e.getBoundingClientRect();const t=e.width||0,i=e.height||0;return{top:e.y,bottom:e.y+i,left:e.x,right:e.x+t,height:i,width:t}}}function Jm(e,t){for(let i in t)t.hasOwnProperty(i)&&(e[i]=t[i]);return e}function eg(e){if("number"!=typeof e&&null!=e){const[t,i]=e.split(Xm);return i&&"px"!==i?null:parseFloat(t)}return e||null}function tg(e){return{top:Math.floor(e.top),right:Math.floor(e.right),bottom:Math.floor(e.bottom),left:Math.floor(e.left),width:Math.floor(e.width),height:Math.floor(e.height)}}class ig{constructor(e,t,i,n,r,s,o){this._preferredPositions=[],this._positionStrategy=new Qm(i,n,r,s,o).withFlexibleDimensions(!1).withPush(!1).withViewportMargin(0),this.withFallbackPosition(e,t),this.onPositionChange=this._positionStrategy.positionChanges}get positions(){return this._preferredPositions}attach(e){this._overlayRef=e,this._positionStrategy.attach(e),this._direction&&(e.setDirection(this._direction),this._direction=null)}dispose(){this._positionStrategy.dispose()}detach(){this._positionStrategy.detach()}apply(){this._positionStrategy.apply()}recalculateLastPosition(){this._positionStrategy.reapplyLastPosition()}withScrollableContainers(e){this._positionStrategy.withScrollableContainers(e)}withFallbackPosition(e,t,i,n){const r=new Um(e,t,i,n);return this._preferredPositions.push(r),this._positionStrategy.withPositions(this._preferredPositions),this}withDirection(e){return this._overlayRef?this._overlayRef.setDirection(e):this._direction=e,this}withOffsetX(e){return this._positionStrategy.withDefaultOffsetX(e),this}withOffsetY(e){return this._positionStrategy.withDefaultOffsetY(e),this}withLockedPosition(e){return this._positionStrategy.withLockedPosition(e),this}withPositions(e){return this._preferredPositions=e.slice(),this._positionStrategy.withPositions(this._preferredPositions),this}setOrigin(e){return this._positionStrategy.setOrigin(e),this}}const ng="cdk-global-overlay-wrapper";class rg{constructor(){this._cssPosition="static",this._topOffset="",this._bottomOffset="",this._leftOffset="",this._rightOffset="",this._alignItems="",this._justifyContent="",this._width="",this._height=""}attach(e){const t=e.getConfig();this._overlayRef=e,this._width&&!t.width&&e.updateSize({width:this._width}),this._height&&!t.height&&e.updateSize({height:this._height}),e.hostElement.classList.add(ng),this._isDisposed=!1}top(e=""){return this._bottomOffset="",this._topOffset=e,this._alignItems="flex-start",this}left(e=""){return this._rightOffset="",this._leftOffset=e,this._justifyContent="flex-start",this}bottom(e=""){return this._topOffset="",this._bottomOffset=e,this._alignItems="flex-end",this}right(e=""){return this._leftOffset="",this._rightOffset=e,this._justifyContent="flex-end",this}width(e=""){return this._overlayRef?this._overlayRef.updateSize({width:e}):this._width=e,this}height(e=""){return this._overlayRef?this._overlayRef.updateSize({height:e}):this._height=e,this}centerHorizontally(e=""){return this.left(e),this._justifyContent="center",this}centerVertically(e=""){return this.top(e),this._alignItems="center",this}apply(){if(!this._overlayRef||!this._overlayRef.hasAttached())return;const e=this._overlayRef.overlayElement.style,t=this._overlayRef.hostElement.style,i=this._overlayRef.getConfig(),{width:n,height:r,maxWidth:s,maxHeight:o}=i,a=!("100%"!==n&&"100vw"!==n||s&&"100%"!==s&&"100vw"!==s),l=!("100%"!==r&&"100vh"!==r||o&&"100%"!==o&&"100vh"!==o);e.position=this._cssPosition,e.marginLeft=a?"0":this._leftOffset,e.marginTop=l?"0":this._topOffset,e.marginBottom=this._bottomOffset,e.marginRight=this._rightOffset,a?t.justifyContent="flex-start":"center"===this._justifyContent?t.justifyContent="center":"rtl"===this._overlayRef.getConfig().direction?"flex-start"===this._justifyContent?t.justifyContent="flex-end":"flex-end"===this._justifyContent&&(t.justifyContent="flex-start"):t.justifyContent=this._justifyContent,t.alignItems=l?"flex-start":this._alignItems}dispose(){if(this._isDisposed||!this._overlayRef)return;const e=this._overlayRef.overlayElement.style,t=this._overlayRef.hostElement,i=t.style;t.classList.remove(ng),i.justifyContent=i.alignItems=e.marginTop=e.marginBottom=e.marginLeft=e.marginRight=e.position="",this._overlayRef=null,this._isDisposed=!0}}let sg=(()=>{class e{constructor(e,t,i,n){this._viewportRuler=e,this._document=t,this._platform=i,this._overlayContainer=n}global(){return new rg}connectedTo(e,t,i){return new ig(t,i,e,this._viewportRuler,this._document,this._platform,this._overlayContainer)}flexibleConnectedTo(e){return new Qm(e,this._viewportRuler,this._document,this._platform,this._overlayContainer)}}return e.\u0275fac=function(t){return new(t||e)(mn(ym),mn(ah),mn(rm),mn(Gm))},e.\u0275prov=pe({factory:function(){return new e(mn(ym),mn(ah),mn(rm),mn(Gm))},token:e,providedIn:"root"}),e})(),og=0,ag=(()=>{class e{constructor(e,t,i,n,r,s,o,a,l,c,h){this.scrollStrategies=e,this._overlayContainer=t,this._componentFactoryResolver=i,this._positionBuilder=n,this._keyboardDispatcher=r,this._injector=s,this._ngZone=o,this._document=a,this._directionality=l,this._location=c,this._outsideClickDispatcher=h}create(e){const t=this._createHostElement(),i=this._createPaneElement(t),n=this._createPortalOutlet(i),r=new Vm(e);return r.direction=r.direction||this._directionality.value,new Zm(n,t,i,r,this._ngZone,this._keyboardDispatcher,this._document,this._location,this._outsideClickDispatcher)}position(){return this._positionBuilder}_createPaneElement(e){const t=this._document.createElement("div");return t.id="cdk-overlay-"+og++,t.classList.add("cdk-overlay-pane"),e.appendChild(t),t}_createHostElement(){const e=this._document.createElement("div");return this._overlayContainer.getContainerElement().appendChild(e),e}_createPortalOutlet(e){return this._appRef||(this._appRef=this._injector.get(Zc)),new Am(e,this._componentFactoryResolver,this._appRef,this._injector,this._document)}}return e.\u0275fac=function(t){return new(t||e)(mn(Nm),mn(Gm),mn(Ma),mn(sg),mn(Wm),mn(oo),mn(Tc),mn(ah),mn(_m),mn(wh),mn($m))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const lg=[{originX:"start",originY:"bottom",overlayX:"start",overlayY:"top"},{originX:"start",originY:"top",overlayX:"start",overlayY:"bottom"},{originX:"end",originY:"top",overlayX:"end",overlayY:"bottom"},{originX:"end",originY:"bottom",overlayX:"end",overlayY:"top"}],cg=new Qi("cdk-connected-overlay-scroll-strategy");let hg=(()=>{class e{constructor(e){this.elementRef=e}}return e.\u0275fac=function(t){return new(t||e)(xo(ja))},e.\u0275dir=Qe({type:e,selectors:[["","cdk-overlay-origin",""],["","overlay-origin",""],["","cdkOverlayOrigin",""]],exportAs:["cdkOverlayOrigin"]}),e})(),ug=(()=>{class e{constructor(e,t,i,n,r){this._overlay=e,this._dir=r,this._hasBackdrop=!1,this._lockPosition=!1,this._growAfterOpen=!1,this._flexibleDimensions=!1,this._push=!1,this._backdropSubscription=u.EMPTY,this._attachSubscription=u.EMPTY,this._detachSubscription=u.EMPTY,this._positionSubscription=u.EMPTY,this.viewportMargin=0,this.open=!1,this.disableClose=!1,this.backdropClick=new Ul,this.positionChange=new Ul,this.attach=new Ul,this.detach=new Ul,this.overlayKeydown=new Ul,this.overlayOutsideClick=new Ul,this._templatePortal=new km(t,i),this._scrollStrategyFactory=n,this.scrollStrategy=this._scrollStrategyFactory()}get offsetX(){return this._offsetX}set offsetX(e){this._offsetX=e,this._position&&this._updatePositionStrategy(this._position)}get offsetY(){return this._offsetY}set offsetY(e){this._offsetY=e,this._position&&this._updatePositionStrategy(this._position)}get hasBackdrop(){return this._hasBackdrop}set hasBackdrop(e){this._hasBackdrop=D_(e)}get lockPosition(){return this._lockPosition}set lockPosition(e){this._lockPosition=D_(e)}get flexibleDimensions(){return this._flexibleDimensions}set flexibleDimensions(e){this._flexibleDimensions=D_(e)}get growAfterOpen(){return this._growAfterOpen}set growAfterOpen(e){this._growAfterOpen=D_(e)}get push(){return this._push}set push(e){this._push=D_(e)}get overlayRef(){return this._overlayRef}get dir(){return this._dir?this._dir.value:"ltr"}ngOnDestroy(){this._attachSubscription.unsubscribe(),this._detachSubscription.unsubscribe(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this._overlayRef&&this._overlayRef.dispose()}ngOnChanges(e){this._position&&(this._updatePositionStrategy(this._position),this._overlayRef.updateSize({width:this.width,minWidth:this.minWidth,height:this.height,minHeight:this.minHeight}),e.origin&&this.open&&this._position.apply()),e.open&&(this.open?this._attachOverlay():this._detachOverlay())}_createOverlay(){this.positions&&this.positions.length||(this.positions=lg);const e=this._overlayRef=this._overlay.create(this._buildConfig());this._attachSubscription=e.attachments().subscribe(()=>this.attach.emit()),this._detachSubscription=e.detachments().subscribe(()=>this.detach.emit()),e.keydownEvents().subscribe(e=>{this.overlayKeydown.next(e),27!==e.keyCode||this.disableClose||Dm(e)||(e.preventDefault(),this._detachOverlay())}),this._overlayRef.outsidePointerEvents().subscribe(e=>{this.overlayOutsideClick.next(e)})}_buildConfig(){const e=this._position=this.positionStrategy||this._createPositionStrategy(),t=new Vm({direction:this._dir,positionStrategy:e,scrollStrategy:this.scrollStrategy,hasBackdrop:this.hasBackdrop});return(this.width||0===this.width)&&(t.width=this.width),(this.height||0===this.height)&&(t.height=this.height),(this.minWidth||0===this.minWidth)&&(t.minWidth=this.minWidth),(this.minHeight||0===this.minHeight)&&(t.minHeight=this.minHeight),this.backdropClass&&(t.backdropClass=this.backdropClass),this.panelClass&&(t.panelClass=this.panelClass),t}_updatePositionStrategy(e){const t=this.positions.map(e=>({originX:e.originX,originY:e.originY,overlayX:e.overlayX,overlayY:e.overlayY,offsetX:e.offsetX||this.offsetX,offsetY:e.offsetY||this.offsetY,panelClass:e.panelClass||void 0}));return e.setOrigin(this.origin.elementRef).withPositions(t).withFlexibleDimensions(this.flexibleDimensions).withPush(this.push).withGrowAfterOpen(this.growAfterOpen).withViewportMargin(this.viewportMargin).withLockedPosition(this.lockPosition).withTransformOriginOn(this.transformOriginSelector)}_createPositionStrategy(){const e=this._overlay.position().flexibleConnectedTo(this.origin.elementRef);return this._updatePositionStrategy(e),e}_attachOverlay(){this._overlayRef?this._overlayRef.getConfig().hasBackdrop=this.hasBackdrop:this._createOverlay(),this._overlayRef.hasAttached()||this._overlayRef.attach(this._templatePortal),this.hasBackdrop?this._backdropSubscription=this._overlayRef.backdropClick().subscribe(e=>{this.backdropClick.emit(e)}):this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe(),this.positionChange.observers.length>0&&(this._positionSubscription=this._position.positionChanges.pipe(function(e,t=!1){return i=>i.lift(new Rm(e,t))}(()=>this.positionChange.observers.length>0)).subscribe(e=>{this.positionChange.emit(e),0===this.positionChange.observers.length&&this._positionSubscription.unsubscribe()}))}_detachOverlay(){this._overlayRef&&this._overlayRef.detach(),this._backdropSubscription.unsubscribe(),this._positionSubscription.unsubscribe()}}return e.\u0275fac=function(t){return new(t||e)(xo(ag),xo(ml),xo(Sl),xo(cg),xo(_m,8))},e.\u0275dir=Qe({type:e,selectors:[["","cdk-connected-overlay",""],["","connected-overlay",""],["","cdkConnectedOverlay",""]],inputs:{viewportMargin:["cdkConnectedOverlayViewportMargin","viewportMargin"],open:["cdkConnectedOverlayOpen","open"],disableClose:["cdkConnectedOverlayDisableClose","disableClose"],scrollStrategy:["cdkConnectedOverlayScrollStrategy","scrollStrategy"],offsetX:["cdkConnectedOverlayOffsetX","offsetX"],offsetY:["cdkConnectedOverlayOffsetY","offsetY"],hasBackdrop:["cdkConnectedOverlayHasBackdrop","hasBackdrop"],lockPosition:["cdkConnectedOverlayLockPosition","lockPosition"],flexibleDimensions:["cdkConnectedOverlayFlexibleDimensions","flexibleDimensions"],growAfterOpen:["cdkConnectedOverlayGrowAfterOpen","growAfterOpen"],push:["cdkConnectedOverlayPush","push"],positions:["cdkConnectedOverlayPositions","positions"],origin:["cdkConnectedOverlayOrigin","origin"],positionStrategy:["cdkConnectedOverlayPositionStrategy","positionStrategy"],width:["cdkConnectedOverlayWidth","width"],height:["cdkConnectedOverlayHeight","height"],minWidth:["cdkConnectedOverlayMinWidth","minWidth"],minHeight:["cdkConnectedOverlayMinHeight","minHeight"],backdropClass:["cdkConnectedOverlayBackdropClass","backdropClass"],panelClass:["cdkConnectedOverlayPanelClass","panelClass"],transformOriginSelector:["cdkConnectedOverlayTransformOriginOn","transformOriginSelector"]},outputs:{backdropClick:"backdropClick",positionChange:"positionChange",attach:"attach",detach:"detach",overlayKeydown:"overlayKeydown",overlayOutsideClick:"overlayOutsideClick"},exportAs:["cdkConnectedOverlay"],features:[dt]}),e})();const dg={provide:cg,deps:[ag],useFactory:function(e){return()=>e.scrollStrategies.reposition()}};let fg=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[ag,dg],imports:[[mm,Om,Cm],Cm]}),e})();function pg(e,t=Hp){return i=>i.lift(new _g(e,t))}class _g{constructor(e,t){this.dueTime=e,this.scheduler=t}call(e,t){return t.subscribe(new mg(e,this.dueTime,this.scheduler))}}class mg extends p{constructor(e,t,i){super(e),this.dueTime=t,this.scheduler=i,this.debouncedSubscription=null,this.lastValue=null,this.hasValue=!1}_next(e){this.clearDebounce(),this.lastValue=e,this.hasValue=!0,this.add(this.debouncedSubscription=this.scheduler.schedule(gg,this.dueTime,this))}_complete(){this.debouncedNext(),this.destination.complete()}debouncedNext(){if(this.clearDebounce(),this.hasValue){const{lastValue:e}=this;this.lastValue=null,this.hasValue=!1,this.destination.next(e)}}clearDebounce(){const e=this.debouncedSubscription;null!==e&&(this.remove(e),e.unsubscribe(),this.debouncedSubscription=null)}}function gg(e){e.debouncedNext()}let vg=(()=>{class e{create(e){return"undefined"==typeof MutationObserver?null:new MutationObserver(e)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({factory:function(){return new e},token:e,providedIn:"root"}),e})(),yg=(()=>{class e{constructor(e){this._mutationObserverFactory=e,this._observedElements=new Map}ngOnDestroy(){this._observedElements.forEach((e,t)=>this._cleanupObserver(t))}observe(e){const t=F_(e);return new v(e=>{const i=this._observeElement(t).subscribe(e);return()=>{i.unsubscribe(),this._unobserveElement(t)}})}_observeElement(e){if(this._observedElements.has(e))this._observedElements.get(e).count++;else{const t=new S,i=this._mutationObserverFactory.create(e=>t.next(e));i&&i.observe(e,{characterData:!0,childList:!0,subtree:!0}),this._observedElements.set(e,{observer:i,stream:t,count:1})}return this._observedElements.get(e).stream}_unobserveElement(e){this._observedElements.has(e)&&(this._observedElements.get(e).count--,this._observedElements.get(e).count||this._cleanupObserver(e))}_cleanupObserver(e){if(this._observedElements.has(e)){const{observer:t,stream:i}=this._observedElements.get(e);t&&t.disconnect(),i.complete(),this._observedElements.delete(e)}}}return e.\u0275fac=function(t){return new(t||e)(mn(vg))},e.\u0275prov=pe({factory:function(){return new e(mn(vg))},token:e,providedIn:"root"}),e})(),bg=(()=>{class e{constructor(e,t,i){this._contentObserver=e,this._elementRef=t,this._ngZone=i,this.event=new Ul,this._disabled=!1,this._currentSubscription=null}get disabled(){return this._disabled}set disabled(e){this._disabled=D_(e),this._disabled?this._unsubscribe():this._subscribe()}get debounce(){return this._debounce}set debounce(e){this._debounce=P_(e),this._subscribe()}ngAfterContentInit(){this._currentSubscription||this.disabled||this._subscribe()}ngOnDestroy(){this._unsubscribe()}_subscribe(){this._unsubscribe();const e=this._contentObserver.observe(this._elementRef);this._ngZone.runOutsideAngular(()=>{this._currentSubscription=(this.debounce?e.pipe(pg(this.debounce)):e).subscribe(this.event)})}_unsubscribe(){var e;null===(e=this._currentSubscription)||void 0===e||e.unsubscribe()}}return e.\u0275fac=function(t){return new(t||e)(xo(yg),xo(ja),xo(Tc))},e.\u0275dir=Qe({type:e,selectors:[["","cdkObserveContent",""]],inputs:{disabled:["cdkObserveContentDisabled","disabled"],debounce:"debounce"},outputs:{event:"cdkObserveContent"},exportAs:["cdkObserveContent"]}),e})(),Cg=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[vg]}),e})();class wg extends class{constructor(e){this._items=e,this._activeItemIndex=-1,this._activeItem=null,this._wrap=!1,this._letterKeyStream=new S,this._typeaheadSubscription=u.EMPTY,this._vertical=!0,this._allowedModifierKeys=[],this._homeAndEnd=!1,this._skipPredicateFn=e=>e.disabled,this._pressedLetters=[],this.tabOut=new S,this.change=new S,e instanceof zl&&e.changes.subscribe(e=>{if(this._activeItem){const t=e.toArray().indexOf(this._activeItem);t>-1&&t!==this._activeItemIndex&&(this._activeItemIndex=t)}})}skipPredicate(e){return this._skipPredicateFn=e,this}withWrap(e=!0){return this._wrap=e,this}withVerticalOrientation(e=!0){return this._vertical=e,this}withHorizontalOrientation(e){return this._horizontal=e,this}withAllowedModifierKeys(e){return this._allowedModifierKeys=e,this}withTypeAhead(e=200){return this._typeaheadSubscription.unsubscribe(),this._typeaheadSubscription=this._letterKeyStream.pipe(Rp(e=>this._pressedLetters.push(e)),pg(e),bp(()=>this._pressedLetters.length>0),M(()=>this._pressedLetters.join(""))).subscribe(e=>{const t=this._getItemsArray();for(let i=1;i!e[t]||this._allowedModifierKeys.indexOf(t)>-1);switch(t){case 9:return void this.tabOut.next();case 40:if(this._vertical&&i){this.setNextItemActive();break}return;case 38:if(this._vertical&&i){this.setPreviousItemActive();break}return;case 39:if(this._horizontal&&i){"rtl"===this._horizontal?this.setPreviousItemActive():this.setNextItemActive();break}return;case 37:if(this._horizontal&&i){"rtl"===this._horizontal?this.setNextItemActive():this.setPreviousItemActive();break}return;case 36:if(this._homeAndEnd&&i){this.setFirstItemActive();break}return;case 35:if(this._homeAndEnd&&i){this.setLastItemActive();break}return;default:return void((i||Dm(e,"shiftKey"))&&(e.key&&1===e.key.length?this._letterKeyStream.next(e.key.toLocaleUpperCase()):(t>=65&&t<=90||t>=48&&t<=57)&&this._letterKeyStream.next(String.fromCharCode(t))))}this._pressedLetters=[],e.preventDefault()}get activeItemIndex(){return this._activeItemIndex}get activeItem(){return this._activeItem}isTyping(){return this._pressedLetters.length>0}setFirstItemActive(){this._setActiveItemByIndex(0,1)}setLastItemActive(){this._setActiveItemByIndex(this._items.length-1,-1)}setNextItemActive(){this._activeItemIndex<0?this.setFirstItemActive():this._setActiveItemByDelta(1)}setPreviousItemActive(){this._activeItemIndex<0&&this._wrap?this.setLastItemActive():this._setActiveItemByDelta(-1)}updateActiveItem(e){const t=this._getItemsArray(),i="number"==typeof e?e:t.indexOf(e),n=t[i];this._activeItem=null==n?null:n,this._activeItemIndex=i}_setActiveItemByDelta(e){this._wrap?this._setActiveInWrapMode(e):this._setActiveInDefaultMode(e)}_setActiveInWrapMode(e){const t=this._getItemsArray();for(let i=1;i<=t.length;i++){const n=(this._activeItemIndex+e*i+t.length)%t.length;if(!this._skipPredicateFn(t[n]))return void this.setActiveItem(n)}}_setActiveInDefaultMode(e){this._setActiveItemByIndex(this._activeItemIndex+e,e)}_setActiveItemByIndex(e,t){const i=this._getItemsArray();if(i[e]){for(;this._skipPredicateFn(i[e]);)if(!i[e+=t])return;this.setActiveItem(e)}}_getItemsArray(){return this._items instanceof zl?this._items.toArray():this._items}}{setActiveItem(e){this.activeItem&&this.activeItem.setInactiveStyles(),super.setActiveItem(e),this.activeItem&&this.activeItem.setActiveStyles()}}"undefined"!=typeof Element&∈const Sg=new Qi("liveAnnouncerElement",{providedIn:"root",factory:function(){return null}}),kg=new Qi("LIVE_ANNOUNCER_DEFAULT_OPTIONS");let xg=(()=>{class e{constructor(e,t,i,n){this._ngZone=t,this._defaultOptions=n,this._document=i,this._liveElement=e||this._createLiveElement()}announce(e,...t){const i=this._defaultOptions;let n,r;return 1===t.length&&"number"==typeof t[0]?r=t[0]:[n,r]=t,this.clear(),clearTimeout(this._previousTimeout),n||(n=i&&i.politeness?i.politeness:"polite"),null==r&&i&&(r=i.duration),this._liveElement.setAttribute("aria-live",n),this._ngZone.runOutsideAngular(()=>new Promise(t=>{clearTimeout(this._previousTimeout),this._previousTimeout=setTimeout(()=>{this._liveElement.textContent=e,t(),"number"==typeof r&&(this._previousTimeout=setTimeout(()=>this.clear(),r))},100)}))}clear(){this._liveElement&&(this._liveElement.textContent="")}ngOnDestroy(){clearTimeout(this._previousTimeout),this._liveElement&&this._liveElement.parentNode&&(this._liveElement.parentNode.removeChild(this._liveElement),this._liveElement=null)}_createLiveElement(){const e="cdk-live-announcer-element",t=this._document.getElementsByClassName(e),i=this._document.createElement("div");for(let n=0;n{class e{constructor(e,t,i,n){this._ngZone=e,this._platform=t,this._origin=null,this._windowFocused=!1,this._elementInfo=new Map,this._monitoredElementCount=0,this._rootNodeFocusListenerCount=new Map,this._documentKeydownListener=()=>{this._lastTouchTarget=null,this._setOriginForCurrentEventQueue("keyboard")},this._documentMousedownListener=e=>{if(!this._lastTouchTarget){const t=Eg(e)?"keyboard":"mouse";this._setOriginForCurrentEventQueue(t)}},this._documentTouchstartListener=e=>{Ag(e)?this._lastTouchTarget||this._setOriginForCurrentEventQueue("keyboard"):(null!=this._touchTimeoutId&&clearTimeout(this._touchTimeoutId),this._lastTouchTarget=Lg(e),this._touchTimeoutId=setTimeout(()=>this._lastTouchTarget=null,650))},this._windowFocusListener=()=>{this._windowFocused=!0,this._windowFocusTimeoutId=setTimeout(()=>this._windowFocused=!1)},this._rootNodeFocusAndBlurListener=e=>{const t=Lg(e),i="focus"===e.type?this._onFocus:this._onBlur;for(let n=t;n;n=n.parentElement)i.call(this,e,n)},this._document=i,this._detectionMode=(null==n?void 0:n.detectionMode)||0}monitor(e,t=!1){const i=F_(e);if(!this._platform.isBrowser||1!==i.nodeType)return mp(null);const n=fm(i)||this._getDocument(),r=this._elementInfo.get(i);if(r)return t&&(r.checkChildren=!0),r.subject;const s={checkChildren:t,subject:new S,rootNode:n};return this._elementInfo.set(i,s),this._registerGlobalListeners(s),s.subject}stopMonitoring(e){const t=F_(e),i=this._elementInfo.get(t);i&&(i.subject.complete(),this._setClasses(t),this._elementInfo.delete(t),this._removeGlobalListeners(i))}focusVia(e,t,i){const n=F_(e);n===this._getDocument().activeElement?this._getClosestElementsInfo(n).forEach(([e,i])=>this._originChanged(e,t,i)):(this._setOriginForCurrentEventQueue(t),"function"==typeof n.focus&&n.focus(i))}ngOnDestroy(){this._elementInfo.forEach((e,t)=>this.stopMonitoring(t))}_getDocument(){return this._document||document}_getWindow(){return this._getDocument().defaultView||window}_toggleClass(e,t,i){i?e.classList.add(t):e.classList.remove(t)}_getFocusOrigin(e){return this._origin?this._origin:this._windowFocused&&this._lastFocusOrigin?this._lastFocusOrigin:this._wasCausedByTouch(e)?"touch":"program"}_setClasses(e,t){this._toggleClass(e,"cdk-focused",!!t),this._toggleClass(e,"cdk-touch-focused","touch"===t),this._toggleClass(e,"cdk-keyboard-focused","keyboard"===t),this._toggleClass(e,"cdk-mouse-focused","mouse"===t),this._toggleClass(e,"cdk-program-focused","program"===t)}_setOriginForCurrentEventQueue(e){this._ngZone.runOutsideAngular(()=>{this._origin=e,0===this._detectionMode&&(this._originTimeoutId=setTimeout(()=>this._origin=null,1))})}_wasCausedByTouch(e){const t=Lg(e);return this._lastTouchTarget instanceof Node&&t instanceof Node&&(t===this._lastTouchTarget||t.contains(this._lastTouchTarget))}_onFocus(e,t){const i=this._elementInfo.get(t);i&&(i.checkChildren||t===Lg(e))&&this._originChanged(t,this._getFocusOrigin(e),i)}_onBlur(e,t){const i=this._elementInfo.get(t);!i||i.checkChildren&&e.relatedTarget instanceof Node&&t.contains(e.relatedTarget)||(this._setClasses(t),this._emitOrigin(i.subject,null))}_emitOrigin(e,t){this._ngZone.run(()=>e.next(t))}_registerGlobalListeners(e){if(!this._platform.isBrowser)return;const t=e.rootNode,i=this._rootNodeFocusListenerCount.get(t)||0;i||this._ngZone.runOutsideAngular(()=>{t.addEventListener("focus",this._rootNodeFocusAndBlurListener,Og),t.addEventListener("blur",this._rootNodeFocusAndBlurListener,Og)}),this._rootNodeFocusListenerCount.set(t,i+1),1==++this._monitoredElementCount&&this._ngZone.runOutsideAngular(()=>{const e=this._getDocument(),t=this._getWindow();e.addEventListener("keydown",this._documentKeydownListener,Og),e.addEventListener("mousedown",this._documentMousedownListener,Og),e.addEventListener("touchstart",this._documentTouchstartListener,Og),t.addEventListener("focus",this._windowFocusListener)})}_removeGlobalListeners(e){const t=e.rootNode;if(this._rootNodeFocusListenerCount.has(t)){const e=this._rootNodeFocusListenerCount.get(t);e>1?this._rootNodeFocusListenerCount.set(t,e-1):(t.removeEventListener("focus",this._rootNodeFocusAndBlurListener,Og),t.removeEventListener("blur",this._rootNodeFocusAndBlurListener,Og),this._rootNodeFocusListenerCount.delete(t))}if(!--this._monitoredElementCount){const e=this._getDocument(),t=this._getWindow();e.removeEventListener("keydown",this._documentKeydownListener,Og),e.removeEventListener("mousedown",this._documentMousedownListener,Og),e.removeEventListener("touchstart",this._documentTouchstartListener,Og),t.removeEventListener("focus",this._windowFocusListener),clearTimeout(this._windowFocusTimeoutId),clearTimeout(this._touchTimeoutId),clearTimeout(this._originTimeoutId)}}_originChanged(e,t,i){this._setClasses(e,t),this._emitOrigin(i.subject,t),this._lastFocusOrigin=t}_getClosestElementsInfo(e){const t=[];return this._elementInfo.forEach((i,n)=>{(n===e||i.checkChildren&&n.contains(e))&&t.push([n,i])}),t}}return e.\u0275fac=function(t){return new(t||e)(mn(Tc),mn(rm),mn(ah,8),mn(Tg,8))},e.\u0275prov=pe({factory:function(){return new e(mn(Tc),mn(rm),mn(ah,8),mn(Tg,8))},token:e,providedIn:"root"}),e})();function Lg(e){return e.composedPath?e.composedPath()[0]:e.target}const Dg="cdk-high-contrast-black-on-white",Pg="cdk-high-contrast-white-on-black",Ig="cdk-high-contrast-active";let Mg=(()=>{class e{constructor(e,t){this._platform=e,this._document=t}getHighContrastMode(){if(!this._platform.isBrowser)return 0;const e=this._document.createElement("div");e.style.backgroundColor="rgb(1,2,3)",e.style.position="absolute",this._document.body.appendChild(e);const t=this._document.defaultView||window,i=t&&t.getComputedStyle?t.getComputedStyle(e):null,n=(i&&i.backgroundColor||"").replace(/ /g,"");switch(this._document.body.removeChild(e),n){case"rgb(0,0,0)":return 2;case"rgb(255,255,255)":return 1}return 0}_applyBodyHighContrastModeCssClasses(){if(this._platform.isBrowser&&this._document.body){const e=this._document.body.classList;e.remove(Ig),e.remove(Dg),e.remove(Pg);const t=this.getHighContrastMode();1===t?(e.add(Ig),e.add(Dg)):2===t&&(e.add(Ig),e.add(Pg))}}}return e.\u0275fac=function(t){return new(t||e)(mn(rm),mn(ah))},e.\u0275prov=pe({factory:function(){return new e(mn(rm),mn(ah))},token:e,providedIn:"root"}),e})();const Fg=new Wa("11.2.6");function Hg(e,t){if(1&e&&Ro(0,"mat-pseudo-checkbox",4),2&e){const e=Uo();Eo("state",e.selected?"checked":"unchecked")("disabled",e.disabled)}}function Bg(e,t){if(1&e&&(To(0,"span",5),da(1),Oo()),2&e){const e=Uo();Zr(1),pa("(",e.group.label,")")}}const jg=["*"],Ng=new Wa("11.2.6"),Vg=new Qi("mat-sanity-checks",{providedIn:"root",factory:function(){return!0}});let Ug,qg=(()=>{class e{constructor(e,t,i){this._hasDoneGlobalChecks=!1,this._document=i,e._applyBodyHighContrastModeCssClasses(),this._sanityChecks=t,this._hasDoneGlobalChecks||(this._checkDoctypeIsDefined(),this._checkThemeIsPresent(),this._checkCdkVersionMatch(),this._hasDoneGlobalChecks=!0)}_getWindow(){const e=this._document.defaultView||window;return"object"==typeof e&&e?e:null}_checksAreEnabled(){return Uc()&&!this._isTestEnv()}_isTestEnv(){const e=this._getWindow();return e&&(e.__karma__||e.jasmine)}_checkDoctypeIsDefined(){this._checksAreEnabled()&&(!0===this._sanityChecks||this._sanityChecks.doctype)&&!this._document.doctype&&console.warn("Current document does not have a doctype. This may cause some Angular Material components not to behave as expected.")}_checkThemeIsPresent(){if(!this._checksAreEnabled()||!1===this._sanityChecks||!this._sanityChecks.theme||!this._document.body||"function"!=typeof getComputedStyle)return;const e=this._document.createElement("div");e.classList.add("mat-theme-loaded-marker"),this._document.body.appendChild(e);const t=getComputedStyle(e);t&&"none"!==t.display&&console.warn("Could not find Angular Material core theme. Most Material components may not work as expected. For more info refer to the theming guide: https://material.angular.io/guide/theming"),this._document.body.removeChild(e)}_checkCdkVersionMatch(){this._checksAreEnabled()&&(!0===this._sanityChecks||this._sanityChecks.version)&&Ng.full!==Fg.full&&console.warn("The Angular Material version ("+Ng.full+") does not match the Angular CDK version ("+Fg.full+").\nPlease ensure the versions of these two packages exactly match.")}}return e.\u0275fac=function(t){return new(t||e)(mn(Mg),mn(Vg,8),mn(ah))},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[mm],mm]}),e})();function zg(e){return class extends e{constructor(...e){super(...e),this._disabled=!1}get disabled(){return this._disabled}set disabled(e){this._disabled=D_(e)}}}function Wg(e,t){return class extends e{constructor(...e){super(...e),this.defaultColor=t,this.color=t}get color(){return this._color}set color(e){const t=e||this.defaultColor;t!==this._color&&(this._color&&this._elementRef.nativeElement.classList.remove("mat-"+this._color),t&&this._elementRef.nativeElement.classList.add("mat-"+t),this._color=t)}}}function $g(e){return class extends e{constructor(...e){super(...e),this._disableRipple=!1}get disableRipple(){return this._disableRipple}set disableRipple(e){this._disableRipple=D_(e)}}}function Kg(e,t=0){return class extends e{constructor(...e){super(...e),this._tabIndex=t,this.defaultTabIndex=t}get tabIndex(){return this.disabled?-1:this._tabIndex}set tabIndex(e){this._tabIndex=null!=e?P_(e):this.defaultTabIndex}}}function Gg(e){return class extends e{constructor(...e){super(...e),this.errorState=!1,this.stateChanges=new S}updateErrorState(){const e=this.errorState,t=(this.errorStateMatcher||this._defaultErrorStateMatcher).isErrorState(this.ngControl?this.ngControl.control:null,this._parentFormGroup||this._parentForm);t!==e&&(this.errorState=t,this.stateChanges.next())}}}try{Ug="undefined"!=typeof Intl}catch(Bx){Ug=!1}let Zg=(()=>{class e{isErrorState(e,t){return!!(e&&e.invalid&&(e.touched||t&&t.submitted))}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({factory:function(){return new e},token:e,providedIn:"root"}),e})();class Yg{constructor(e,t,i){this._renderer=e,this.element=t,this.config=i,this.state=3}fadeOut(){this._renderer.fadeOutRipple(this)}}const Xg={enterDuration:450,exitDuration:400},Qg=um({passive:!0}),Jg=["mousedown","touchstart"],ev=["mouseup","mouseleave","touchend","touchcancel"];class tv{constructor(e,t,i,n){this._target=e,this._ngZone=t,this._isPointerDown=!1,this._activeRipples=new Set,this._pointerUpEventsRegistered=!1,n.isBrowser&&(this._containerElement=F_(i))}fadeInRipple(e,t,i={}){const n=this._containerRect=this._containerRect||this._containerElement.getBoundingClientRect(),r=Object.assign(Object.assign({},Xg),i.animation);i.centered&&(e=n.left+n.width/2,t=n.top+n.height/2);const s=i.radius||function(e,t,i){const n=Math.max(Math.abs(e-i.left),Math.abs(e-i.right)),r=Math.max(Math.abs(t-i.top),Math.abs(t-i.bottom));return Math.sqrt(n*n+r*r)}(e,t,n),o=e-n.left,a=t-n.top,l=r.enterDuration,c=document.createElement("div");c.classList.add("mat-ripple-element"),c.style.left=o-s+"px",c.style.top=a-s+"px",c.style.height=2*s+"px",c.style.width=2*s+"px",null!=i.color&&(c.style.backgroundColor=i.color),c.style.transitionDuration=l+"ms",this._containerElement.appendChild(c),window.getComputedStyle(c).getPropertyValue("opacity"),c.style.transform="scale(1)";const h=new Yg(this,c,i);return h.state=0,this._activeRipples.add(h),i.persistent||(this._mostRecentTransientRipple=h),this._runTimeoutOutsideZone(()=>{const e=h===this._mostRecentTransientRipple;h.state=1,i.persistent||e&&this._isPointerDown||h.fadeOut()},l),h}fadeOutRipple(e){const t=this._activeRipples.delete(e);if(e===this._mostRecentTransientRipple&&(this._mostRecentTransientRipple=null),this._activeRipples.size||(this._containerRect=null),!t)return;const i=e.element,n=Object.assign(Object.assign({},Xg),e.config.animation);i.style.transitionDuration=n.exitDuration+"ms",i.style.opacity="0",e.state=2,this._runTimeoutOutsideZone(()=>{e.state=3,i.parentNode.removeChild(i)},n.exitDuration)}fadeOutAll(){this._activeRipples.forEach(e=>e.fadeOut())}setupTriggerEvents(e){const t=F_(e);t&&t!==this._triggerElement&&(this._removeTriggerEvents(),this._triggerElement=t,this._registerEvents(Jg))}handleEvent(e){"mousedown"===e.type?this._onMousedown(e):"touchstart"===e.type?this._onTouchStart(e):this._onPointerUp(),this._pointerUpEventsRegistered||(this._registerEvents(ev),this._pointerUpEventsRegistered=!0)}_onMousedown(e){const t=Eg(e),i=this._lastTouchStartEvent&&Date.now(){!e.config.persistent&&(1===e.state||e.config.terminateOnPointerUp&&0===e.state)&&e.fadeOut()}))}_runTimeoutOutsideZone(e,t=0){this._ngZone.runOutsideAngular(()=>setTimeout(e,t))}_registerEvents(e){this._ngZone.runOutsideAngular(()=>{e.forEach(e=>{this._triggerElement.addEventListener(e,this,Qg)})})}_removeTriggerEvents(){this._triggerElement&&(Jg.forEach(e=>{this._triggerElement.removeEventListener(e,this,Qg)}),this._pointerUpEventsRegistered&&ev.forEach(e=>{this._triggerElement.removeEventListener(e,this,Qg)}))}}const iv=new Qi("mat-ripple-global-options");let nv=(()=>{class e{constructor(e,t,i,n,r){this._elementRef=e,this._animationMode=r,this.radius=0,this._disabled=!1,this._isInitialized=!1,this._globalOptions=n||{},this._rippleRenderer=new tv(this,t,e,i)}get disabled(){return this._disabled}set disabled(e){this._disabled=e,this._setupTriggerEventsIfEnabled()}get trigger(){return this._trigger||this._elementRef.nativeElement}set trigger(e){this._trigger=e,this._setupTriggerEventsIfEnabled()}ngOnInit(){this._isInitialized=!0,this._setupTriggerEventsIfEnabled()}ngOnDestroy(){this._rippleRenderer._removeTriggerEvents()}fadeOutAll(){this._rippleRenderer.fadeOutAll()}get rippleConfig(){return{centered:this.centered,radius:this.radius,color:this.color,animation:Object.assign(Object.assign(Object.assign({},this._globalOptions.animation),"NoopAnimations"===this._animationMode?{enterDuration:0,exitDuration:0}:{}),this.animation),terminateOnPointerUp:this._globalOptions.terminateOnPointerUp}}get rippleDisabled(){return this.disabled||!!this._globalOptions.disabled}_setupTriggerEventsIfEnabled(){!this.disabled&&this._isInitialized&&this._rippleRenderer.setupTriggerEvents(this.trigger)}launch(e,t=0,i){return"number"==typeof e?this._rippleRenderer.fadeInRipple(e,t,Object.assign(Object.assign({},this.rippleConfig),i)):this._rippleRenderer.fadeInRipple(0,0,Object.assign(Object.assign({},this.rippleConfig),e))}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(Tc),xo(rm),xo(iv,8),xo(ap,8))},e.\u0275dir=Qe({type:e,selectors:[["","mat-ripple",""],["","matRipple",""]],hostAttrs:[1,"mat-ripple"],hostVars:2,hostBindings:function(e,t){2&e&&Jo("mat-ripple-unbounded",t.unbounded)},inputs:{radius:["matRippleRadius","radius"],disabled:["matRippleDisabled","disabled"],trigger:["matRippleTrigger","trigger"],color:["matRippleColor","color"],unbounded:["matRippleUnbounded","unbounded"],centered:["matRippleCentered","centered"],animation:["matRippleAnimation","animation"]},exportAs:["matRipple"]}),e})(),rv=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[qg,sm],qg]}),e})(),sv=(()=>{class e{constructor(e){this._animationMode=e,this.state="unchecked",this.disabled=!1}}return e.\u0275fac=function(t){return new(t||e)(xo(ap,8))},e.\u0275cmp=$e({type:e,selectors:[["mat-pseudo-checkbox"]],hostAttrs:[1,"mat-pseudo-checkbox"],hostVars:8,hostBindings:function(e,t){2&e&&Jo("mat-pseudo-checkbox-indeterminate","indeterminate"===t.state)("mat-pseudo-checkbox-checked","checked"===t.state)("mat-pseudo-checkbox-disabled",t.disabled)("_mat-animation-noopable","NoopAnimations"===t._animationMode)},inputs:{state:"state",disabled:"disabled"},decls:0,vars:0,template:function(e,t){},styles:['.mat-pseudo-checkbox{width:16px;height:16px;border:2px solid;border-radius:2px;cursor:pointer;display:inline-block;vertical-align:middle;box-sizing:border-box;position:relative;flex-shrink:0;transition:border-color 90ms cubic-bezier(0, 0, 0.2, 0.1),background-color 90ms cubic-bezier(0, 0, 0.2, 0.1)}.mat-pseudo-checkbox::after{position:absolute;opacity:0;content:"";border-bottom:2px solid currentColor;transition:opacity 90ms cubic-bezier(0, 0, 0.2, 0.1)}.mat-pseudo-checkbox.mat-pseudo-checkbox-checked,.mat-pseudo-checkbox.mat-pseudo-checkbox-indeterminate{border-color:transparent}._mat-animation-noopable.mat-pseudo-checkbox{transition:none;animation:none}._mat-animation-noopable.mat-pseudo-checkbox::after{transition:none}.mat-pseudo-checkbox-disabled{cursor:default}.mat-pseudo-checkbox-indeterminate::after{top:5px;left:1px;width:10px;opacity:1;border-radius:2px}.mat-pseudo-checkbox-checked::after{top:2.4px;left:1px;width:8px;height:3px;border-left:2px solid currentColor;transform:rotate(-45deg);opacity:1;box-sizing:content-box}\n'],encapsulation:2,changeDetection:0}),e})(),ov=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[qg]]}),e})();const av=new Qi("MAT_OPTION_PARENT_COMPONENT");class lv{}const cv=zg(lv);let hv=0,uv=(()=>{class e extends cv{constructor(e){var t;super(),this._labelId="mat-optgroup-label-"+hv++,this._inert=null!==(t=null==e?void 0:e.inertGroups)&&void 0!==t&&t}}return e.\u0275fac=function(t){return new(t||e)(xo(av,8))},e.\u0275dir=Qe({type:e,inputs:{label:"label"},features:[lo]}),e})();const dv=new Qi("MatOptgroup");let fv=0;class pv{constructor(e,t=!1){this.source=e,this.isUserInput=t}}let _v=(()=>{class e{constructor(e,t,i,n){this._element=e,this._changeDetectorRef=t,this._parent=i,this.group=n,this._selected=!1,this._active=!1,this._disabled=!1,this._mostRecentViewValue="",this.id="mat-option-"+fv++,this.onSelectionChange=new Ul,this._stateChanges=new S}get multiple(){return this._parent&&this._parent.multiple}get selected(){return this._selected}get disabled(){return this.group&&this.group.disabled||this._disabled}set disabled(e){this._disabled=D_(e)}get disableRipple(){return this._parent&&this._parent.disableRipple}get active(){return this._active}get viewValue(){return(this._getHostElement().textContent||"").trim()}select(){this._selected||(this._selected=!0,this._changeDetectorRef.markForCheck(),this._emitSelectionChangeEvent())}deselect(){this._selected&&(this._selected=!1,this._changeDetectorRef.markForCheck(),this._emitSelectionChangeEvent())}focus(e,t){const i=this._getHostElement();"function"==typeof i.focus&&i.focus(t)}setActiveStyles(){this._active||(this._active=!0,this._changeDetectorRef.markForCheck())}setInactiveStyles(){this._active&&(this._active=!1,this._changeDetectorRef.markForCheck())}getLabel(){return this.viewValue}_handleKeydown(e){13!==e.keyCode&&32!==e.keyCode||Dm(e)||(this._selectViaInteraction(),e.preventDefault())}_selectViaInteraction(){this.disabled||(this._selected=!this.multiple||!this._selected,this._changeDetectorRef.markForCheck(),this._emitSelectionChangeEvent(!0))}_getAriaSelected(){return this.selected||!this.multiple&&null}_getTabIndex(){return this.disabled?"-1":"0"}_getHostElement(){return this._element.nativeElement}ngAfterViewChecked(){if(this._selected){const e=this.viewValue;e!==this._mostRecentViewValue&&(this._mostRecentViewValue=e,this._stateChanges.next())}}ngOnDestroy(){this._stateChanges.complete()}_emitSelectionChangeEvent(e=!1){this.onSelectionChange.emit(new pv(this,e))}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(hl),xo(void 0),xo(uv))},e.\u0275dir=Qe({type:e,inputs:{id:"id",disabled:"disabled",value:"value"},outputs:{onSelectionChange:"onSelectionChange"}}),e})(),mv=(()=>{class e extends _v{constructor(e,t,i,n){super(e,t,i,n)}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(hl),xo(av,8),xo(dv,8))},e.\u0275cmp=$e({type:e,selectors:[["mat-option"]],hostAttrs:["role","option",1,"mat-option","mat-focus-indicator"],hostVars:12,hostBindings:function(e,t){1&e&&Ho("click",(function(){return t._selectViaInteraction()}))("keydown",(function(e){return t._handleKeydown(e)})),2&e&&(_a("id",t.id),Co("tabindex",t._getTabIndex())("aria-selected",t._getAriaSelected())("aria-disabled",t.disabled.toString()),Jo("mat-selected",t.selected)("mat-option-multiple",t.multiple)("mat-active",t.active)("mat-option-disabled",t.disabled))},exportAs:["matOption"],features:[lo],ngContentSelectors:jg,decls:5,vars:4,consts:[["class","mat-option-pseudo-checkbox",3,"state","disabled",4,"ngIf"],[1,"mat-option-text"],["class","cdk-visually-hidden",4,"ngIf"],["mat-ripple","",1,"mat-option-ripple",3,"matRippleTrigger","matRippleDisabled"],[1,"mat-option-pseudo-checkbox",3,"state","disabled"],[1,"cdk-visually-hidden"]],template:function(e,t){1&e&&(zo(),So(0,Hg,1,2,"mat-pseudo-checkbox",0),To(1,"span",1),Wo(2),Oo(),So(3,Bg,2,1,"span",2),Ro(4,"div",3)),2&e&&(Eo("ngIf",t.multiple),Zr(3),Eo("ngIf",t.group&&t.group._inert),Zr(1),Eo("matRippleTrigger",t._getHostElement())("matRippleDisabled",t.disabled||t.disableRipple))},directives:[Ph,nv,sv],styles:[".mat-option{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;line-height:48px;height:48px;padding:0 16px;text-align:left;text-decoration:none;max-width:100%;position:relative;cursor:pointer;outline:none;display:flex;flex-direction:row;max-width:100%;box-sizing:border-box;align-items:center;-webkit-tap-highlight-color:transparent}.mat-option[disabled]{cursor:default}[dir=rtl] .mat-option{text-align:right}.mat-option .mat-icon{margin-right:16px;vertical-align:middle}.mat-option .mat-icon svg{vertical-align:top}[dir=rtl] .mat-option .mat-icon{margin-left:16px;margin-right:0}.mat-option[aria-disabled=true]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.mat-optgroup .mat-option:not(.mat-option-multiple){padding-left:32px}[dir=rtl] .mat-optgroup .mat-option:not(.mat-option-multiple){padding-left:16px;padding-right:32px}.cdk-high-contrast-active .mat-option{margin:0 1px}.cdk-high-contrast-active .mat-option.mat-active{border:solid 1px currentColor;margin:0}.cdk-high-contrast-active .mat-option[aria-disabled=true]{opacity:.5}.mat-option-text{display:inline-block;flex-grow:1;overflow:hidden;text-overflow:ellipsis}.mat-option .mat-option-ripple{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none}.mat-option-pseudo-checkbox{margin-right:8px}[dir=rtl] .mat-option-pseudo-checkbox{margin-left:8px;margin-right:0}\n"],encapsulation:2,changeDetection:0}),e})();function gv(e,t,i){if(i.length){let n=t.toArray(),r=i.toArray(),s=0;for(let t=0;t{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[rv,Wh,qg,ov]]}),e})();const yv=["mat-button",""],bv=["*"],Cv=".mat-button .mat-button-focus-overlay,.mat-icon-button .mat-button-focus-overlay{opacity:0}.mat-button:hover:not(.mat-button-disabled) .mat-button-focus-overlay,.mat-stroked-button:hover:not(.mat-button-disabled) .mat-button-focus-overlay{opacity:.04}@media(hover: none){.mat-button:hover:not(.mat-button-disabled) .mat-button-focus-overlay,.mat-stroked-button:hover:not(.mat-button-disabled) .mat-button-focus-overlay{opacity:0}}.mat-button,.mat-icon-button,.mat-stroked-button,.mat-flat-button{box-sizing:border-box;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent;display:inline-block;white-space:nowrap;text-decoration:none;vertical-align:baseline;text-align:center;margin:0;min-width:64px;line-height:36px;padding:0 16px;border-radius:4px;overflow:visible}.mat-button::-moz-focus-inner,.mat-icon-button::-moz-focus-inner,.mat-stroked-button::-moz-focus-inner,.mat-flat-button::-moz-focus-inner{border:0}.mat-button.mat-button-disabled,.mat-icon-button.mat-button-disabled,.mat-stroked-button.mat-button-disabled,.mat-flat-button.mat-button-disabled{cursor:default}.mat-button.cdk-keyboard-focused .mat-button-focus-overlay,.mat-button.cdk-program-focused .mat-button-focus-overlay,.mat-icon-button.cdk-keyboard-focused .mat-button-focus-overlay,.mat-icon-button.cdk-program-focused .mat-button-focus-overlay,.mat-stroked-button.cdk-keyboard-focused .mat-button-focus-overlay,.mat-stroked-button.cdk-program-focused .mat-button-focus-overlay,.mat-flat-button.cdk-keyboard-focused .mat-button-focus-overlay,.mat-flat-button.cdk-program-focused .mat-button-focus-overlay{opacity:.12}.mat-button::-moz-focus-inner,.mat-icon-button::-moz-focus-inner,.mat-stroked-button::-moz-focus-inner,.mat-flat-button::-moz-focus-inner{border:0}.mat-raised-button{box-sizing:border-box;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent;display:inline-block;white-space:nowrap;text-decoration:none;vertical-align:baseline;text-align:center;margin:0;min-width:64px;line-height:36px;padding:0 16px;border-radius:4px;overflow:visible;transform:translate3d(0, 0, 0);transition:background 400ms cubic-bezier(0.25, 0.8, 0.25, 1),box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mat-raised-button::-moz-focus-inner{border:0}.mat-raised-button.mat-button-disabled{cursor:default}.mat-raised-button.cdk-keyboard-focused .mat-button-focus-overlay,.mat-raised-button.cdk-program-focused .mat-button-focus-overlay{opacity:.12}.mat-raised-button::-moz-focus-inner{border:0}._mat-animation-noopable.mat-raised-button{transition:none;animation:none}.mat-stroked-button{border:1px solid currentColor;padding:0 15px;line-height:34px}.mat-stroked-button .mat-button-ripple.mat-ripple,.mat-stroked-button .mat-button-focus-overlay{top:-1px;left:-1px;right:-1px;bottom:-1px}.mat-fab{box-sizing:border-box;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent;display:inline-block;white-space:nowrap;text-decoration:none;vertical-align:baseline;text-align:center;margin:0;min-width:64px;line-height:36px;padding:0 16px;border-radius:4px;overflow:visible;transform:translate3d(0, 0, 0);transition:background 400ms cubic-bezier(0.25, 0.8, 0.25, 1),box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);min-width:0;border-radius:50%;width:56px;height:56px;padding:0;flex-shrink:0}.mat-fab::-moz-focus-inner{border:0}.mat-fab.mat-button-disabled{cursor:default}.mat-fab.cdk-keyboard-focused .mat-button-focus-overlay,.mat-fab.cdk-program-focused .mat-button-focus-overlay{opacity:.12}.mat-fab::-moz-focus-inner{border:0}._mat-animation-noopable.mat-fab{transition:none;animation:none}.mat-fab .mat-button-wrapper{padding:16px 0;display:inline-block;line-height:24px}.mat-mini-fab{box-sizing:border-box;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;outline:none;border:none;-webkit-tap-highlight-color:transparent;display:inline-block;white-space:nowrap;text-decoration:none;vertical-align:baseline;text-align:center;margin:0;min-width:64px;line-height:36px;padding:0 16px;border-radius:4px;overflow:visible;transform:translate3d(0, 0, 0);transition:background 400ms cubic-bezier(0.25, 0.8, 0.25, 1),box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);min-width:0;border-radius:50%;width:40px;height:40px;padding:0;flex-shrink:0}.mat-mini-fab::-moz-focus-inner{border:0}.mat-mini-fab.mat-button-disabled{cursor:default}.mat-mini-fab.cdk-keyboard-focused .mat-button-focus-overlay,.mat-mini-fab.cdk-program-focused .mat-button-focus-overlay{opacity:.12}.mat-mini-fab::-moz-focus-inner{border:0}._mat-animation-noopable.mat-mini-fab{transition:none;animation:none}.mat-mini-fab .mat-button-wrapper{padding:8px 0;display:inline-block;line-height:24px}.mat-icon-button{padding:0;min-width:0;width:40px;height:40px;flex-shrink:0;line-height:40px;border-radius:50%}.mat-icon-button i,.mat-icon-button .mat-icon{line-height:24px}.mat-button-ripple.mat-ripple,.mat-button-focus-overlay{top:0;left:0;right:0;bottom:0;position:absolute;pointer-events:none;border-radius:inherit}.mat-button-ripple.mat-ripple:not(:empty){transform:translateZ(0)}.mat-button-focus-overlay{opacity:0;transition:opacity 200ms cubic-bezier(0.35, 0, 0.25, 1),background-color 200ms cubic-bezier(0.35, 0, 0.25, 1)}._mat-animation-noopable .mat-button-focus-overlay{transition:none}.mat-button-ripple-round{border-radius:50%;z-index:1}.mat-button .mat-button-wrapper>*,.mat-flat-button .mat-button-wrapper>*,.mat-stroked-button .mat-button-wrapper>*,.mat-raised-button .mat-button-wrapper>*,.mat-icon-button .mat-button-wrapper>*,.mat-fab .mat-button-wrapper>*,.mat-mini-fab .mat-button-wrapper>*{vertical-align:middle}.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon-button,.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon-button{display:inline-flex;justify-content:center;align-items:center;font-size:inherit;width:2.5em;height:2.5em}.cdk-high-contrast-active .mat-button,.cdk-high-contrast-active .mat-flat-button,.cdk-high-contrast-active .mat-raised-button,.cdk-high-contrast-active .mat-icon-button,.cdk-high-contrast-active .mat-fab,.cdk-high-contrast-active .mat-mini-fab{outline:solid 1px}.cdk-high-contrast-active .mat-button-base.cdk-keyboard-focused,.cdk-high-contrast-active .mat-button-base.cdk-program-focused{outline:solid 3px}\n",wv=["mat-button","mat-flat-button","mat-icon-button","mat-raised-button","mat-stroked-button","mat-mini-fab","mat-fab"];class Sv{constructor(e){this._elementRef=e}}const kv=Wg(zg($g(Sv)));let xv=(()=>{class e extends kv{constructor(e,t,i){super(e),this._focusMonitor=t,this._animationMode=i,this.isRoundButton=this._hasHostAttributes("mat-fab","mat-mini-fab"),this.isIconButton=this._hasHostAttributes("mat-icon-button");for(const n of wv)this._hasHostAttributes(n)&&this._getHostElement().classList.add(n);e.nativeElement.classList.add("mat-button-base"),this.isRoundButton&&(this.color="accent")}ngAfterViewInit(){this._focusMonitor.monitor(this._elementRef,!0)}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef)}focus(e,t){e?this._focusMonitor.focusVia(this._getHostElement(),e,t):this._getHostElement().focus(t)}_getHostElement(){return this._elementRef.nativeElement}_isRippleDisabled(){return this.disableRipple||this.disabled}_hasHostAttributes(...e){return e.some(e=>this._getHostElement().hasAttribute(e))}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(Rg),xo(ap,8))},e.\u0275cmp=$e({type:e,selectors:[["button","mat-button",""],["button","mat-raised-button",""],["button","mat-icon-button",""],["button","mat-fab",""],["button","mat-mini-fab",""],["button","mat-stroked-button",""],["button","mat-flat-button",""]],viewQuery:function(e,t){if(1&e&&ec(nv,1),2&e){let e;Jl(e=ic())&&(t.ripple=e.first)}},hostAttrs:[1,"mat-focus-indicator"],hostVars:5,hostBindings:function(e,t){2&e&&(Co("disabled",t.disabled||null),Jo("_mat-animation-noopable","NoopAnimations"===t._animationMode)("mat-button-disabled",t.disabled))},inputs:{disabled:"disabled",disableRipple:"disableRipple",color:"color"},exportAs:["matButton"],features:[lo],attrs:yv,ngContentSelectors:bv,decls:4,vars:5,consts:[[1,"mat-button-wrapper"],["matRipple","",1,"mat-button-ripple",3,"matRippleDisabled","matRippleCentered","matRippleTrigger"],[1,"mat-button-focus-overlay"]],template:function(e,t){1&e&&(zo(),To(0,"span",0),Wo(1),Oo(),Ro(2,"span",1),Ro(3,"span",2)),2&e&&(Zr(2),Jo("mat-button-ripple-round",t.isRoundButton||t.isIconButton),Eo("matRippleDisabled",t._isRippleDisabled())("matRippleCentered",t.isIconButton)("matRippleTrigger",t._getHostElement()))},directives:[nv],styles:[Cv],encapsulation:2,changeDetection:0}),e})(),Ev=(()=>{class e extends xv{constructor(e,t,i){super(t,e,i)}_haltDisabledEvents(e){this.disabled&&(e.preventDefault(),e.stopImmediatePropagation())}}return e.\u0275fac=function(t){return new(t||e)(xo(Rg),xo(ja),xo(ap,8))},e.\u0275cmp=$e({type:e,selectors:[["a","mat-button",""],["a","mat-raised-button",""],["a","mat-icon-button",""],["a","mat-fab",""],["a","mat-mini-fab",""],["a","mat-stroked-button",""],["a","mat-flat-button",""]],hostAttrs:[1,"mat-focus-indicator"],hostVars:7,hostBindings:function(e,t){1&e&&Ho("click",(function(e){return t._haltDisabledEvents(e)})),2&e&&(Co("tabindex",t.disabled?-1:t.tabIndex||0)("disabled",t.disabled||null)("aria-disabled",t.disabled.toString()),Jo("_mat-animation-noopable","NoopAnimations"===t._animationMode)("mat-button-disabled",t.disabled))},inputs:{disabled:"disabled",disableRipple:"disableRipple",color:"color",tabIndex:"tabIndex"},exportAs:["matButton","matAnchor"],features:[lo],attrs:yv,ngContentSelectors:bv,decls:4,vars:5,consts:[[1,"mat-button-wrapper"],["matRipple","",1,"mat-button-ripple",3,"matRippleDisabled","matRippleCentered","matRippleTrigger"],[1,"mat-button-focus-overlay"]],template:function(e,t){1&e&&(zo(),To(0,"span",0),Wo(1),Oo(),Ro(2,"span",1),Ro(3,"span",2)),2&e&&(Zr(2),Jo("mat-button-ripple-round",t.isRoundButton||t.isIconButton),Eo("matRippleDisabled",t._isRippleDisabled())("matRippleCentered",t.isIconButton)("matRippleTrigger",t._getHostElement()))},directives:[nv],styles:[Cv],encapsulation:2,changeDetection:0}),e})(),Av=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[rv,qg],qg]}),e})();const Tv={};function Ov(...e){let t=null,i=null;return x(e[e.length-1])&&(i=e.pop()),"function"==typeof e[e.length-1]&&(t=e.pop()),1===e.length&&l(e[0])&&(e=e[0]),z(e,i).lift(new Rv(t))}class Rv{constructor(e){this.resultSelector=e}call(e,t){return t.subscribe(new Lv(e,this.resultSelector))}}class Lv extends I{constructor(e,t){super(e),this.resultSelector=t,this.active=0,this.values=[],this.observables=[]}_next(e){this.values.push(Tv),this.observables.push(e)}_complete(){const e=this.observables,t=e.length;if(0===t)this.destination.complete();else{this.active=t,this.toRespond=t;for(let i=0;ithis.total&&this.destination.next(e)}}const Iv=new Set;let Mv,Fv=(()=>{class e{constructor(e){this._platform=e,this._matchMedia=this._platform.isBrowser&&window.matchMedia?window.matchMedia.bind(window):Hv}matchMedia(e){return this._platform.WEBKIT&&function(e){if(!Iv.has(e))try{Mv||(Mv=document.createElement("style"),Mv.setAttribute("type","text/css"),document.head.appendChild(Mv)),Mv.sheet&&(Mv.sheet.insertRule(`@media ${e} {.fx-query-test{ }}`,0),Iv.add(e))}catch(t){console.error(t)}}(e),this._matchMedia(e)}}return e.\u0275fac=function(t){return new(t||e)(mn(rm))},e.\u0275prov=pe({factory:function(){return new e(mn(rm))},token:e,providedIn:"root"}),e})();function Hv(e){return{matches:"all"===e||""===e,media:e,addListener:()=>{},removeListener:()=>{}}}let Bv=(()=>{class e{constructor(e,t){this._mediaMatcher=e,this._zone=t,this._queries=new Map,this._destroySubject=new S}ngOnDestroy(){this._destroySubject.next(),this._destroySubject.complete()}isMatched(e){return jv(I_(e)).some(e=>this._registerQuery(e).mql.matches)}observe(e){let t=Ov(jv(I_(e)).map(e=>this._registerQuery(e).observable));return t=gp(t.pipe(Ap(1)),t.pipe(e=>e.lift(new Dv(1)),pg(0))),t.pipe(M(e=>{const t={matches:!1,breakpoints:{}};return e.forEach(({matches:e,query:i})=>{t.matches=t.matches||e,t.breakpoints[i]=e}),t}))}_registerQuery(e){if(this._queries.has(e))return this._queries.get(e);const t=this._mediaMatcher.matchMedia(e),i={observable:new v(e=>{const i=t=>this._zone.run(()=>e.next(t));return t.addListener(i),()=>{t.removeListener(i)}}).pipe(K_(t),M(({matches:t})=>({query:e,matches:t})),z_(this._destroySubject)),mql:t};return this._queries.set(e,i),i}}return e.\u0275fac=function(t){return new(t||e)(mn(Fv),mn(Tc))},e.\u0275prov=pe({factory:function(){return new e(mn(Fv),mn(Tc))},token:e,providedIn:"root"}),e})();function jv(e){return e.map(e=>e.split(",")).reduce((e,t)=>e.concat(t)).map(e=>e.trim())}function Nv(e,t){if(1&e){const e=Po();To(0,"div",1),To(1,"button",2),Ho("click",(function(){return Bt(e),Uo().action()})),da(2),Oo(),Oo()}if(2&e){const e=Uo();Zr(2),fa(e.data.action)}}function Vv(e,t){}const Uv=new Qi("MatSnackBarData");class qv{constructor(){this.politeness="assertive",this.announcementMessage="",this.duration=0,this.data=null,this.horizontalPosition="center",this.verticalPosition="bottom"}}const zv=Math.pow(2,31)-1;class Wv{constructor(e,t){this._overlayRef=t,this._afterDismissed=new S,this._afterOpened=new S,this._onAction=new S,this._dismissedByAction=!1,this.containerInstance=e,this.onAction().subscribe(()=>this.dismiss()),e._onExit.subscribe(()=>this._finishDismiss())}dismiss(){this._afterDismissed.closed||this.containerInstance.exit(),clearTimeout(this._durationTimeoutId)}dismissWithAction(){this._onAction.closed||(this._dismissedByAction=!0,this._onAction.next(),this._onAction.complete())}closeWithAction(){this.dismissWithAction()}_dismissAfter(e){this._durationTimeoutId=setTimeout(()=>this.dismiss(),Math.min(e,zv))}_open(){this._afterOpened.closed||(this._afterOpened.next(),this._afterOpened.complete())}_finishDismiss(){this._overlayRef.dispose(),this._onAction.closed||this._onAction.complete(),this._afterDismissed.next({dismissedByAction:this._dismissedByAction}),this._afterDismissed.complete(),this._dismissedByAction=!1}afterDismissed(){return this._afterDismissed}afterOpened(){return this.containerInstance._onEnter}onAction(){return this._onAction}}let $v=(()=>{class e{constructor(e,t){this.snackBarRef=e,this.data=t}action(){this.snackBarRef.dismissWithAction()}get hasAction(){return!!this.data.action}}return e.\u0275fac=function(t){return new(t||e)(xo(Wv),xo(Uv))},e.\u0275cmp=$e({type:e,selectors:[["simple-snack-bar"]],hostAttrs:[1,"mat-simple-snackbar"],decls:3,vars:2,consts:[["class","mat-simple-snackbar-action",4,"ngIf"],[1,"mat-simple-snackbar-action"],["mat-button","",3,"click"]],template:function(e,t){1&e&&(To(0,"span"),da(1),Oo(),So(2,Nv,3,1,"div",0)),2&e&&(Zr(1),fa(t.data.message),Zr(1),Eo("ngIf",t.hasAction))},directives:[Ph,xv],styles:[".mat-simple-snackbar{display:flex;justify-content:space-between;align-items:center;line-height:20px;opacity:1}.mat-simple-snackbar-action{flex-shrink:0;margin:-8px -8px -8px 8px}.mat-simple-snackbar-action button{max-height:36px;min-width:0}[dir=rtl] .mat-simple-snackbar-action{margin-left:-8px;margin-right:8px}\n"],encapsulation:2,changeDetection:0}),e})();const Kv={snackBarState:Eu("state",[Ru("void, hidden",Ou({transform:"scale(0.8)",opacity:0})),Ru("visible",Ou({transform:"scale(1)",opacity:1})),Lu("* => visible",Au("150ms cubic-bezier(0, 0, 0.2, 1)")),Lu("* => void, * => hidden",Au("75ms cubic-bezier(0.4, 0.0, 1, 1)",Ou({opacity:0})))])};let Gv=(()=>{class e extends Em{constructor(e,t,i,n,r){super(),this._ngZone=e,this._elementRef=t,this._changeDetectorRef=i,this._platform=n,this.snackBarConfig=r,this._announceDelay=150,this._destroyed=!1,this._onAnnounce=new S,this._onExit=new S,this._onEnter=new S,this._animationState="void",this.attachDomPortal=e=>(this._assertNotAttached(),this._applySnackBarClasses(),this._portalOutlet.attachDomPortal(e)),this._live="assertive"!==r.politeness||r.announcementMessage?"off"===r.politeness?"off":"polite":"assertive",this._platform.FIREFOX&&("polite"===this._live&&(this._role="status"),"assertive"===this._live&&(this._role="alert"))}attachComponentPortal(e){return this._assertNotAttached(),this._applySnackBarClasses(),this._portalOutlet.attachComponentPortal(e)}attachTemplatePortal(e){return this._assertNotAttached(),this._applySnackBarClasses(),this._portalOutlet.attachTemplatePortal(e)}onAnimationEnd(e){const{fromState:t,toState:i}=e;if(("void"===i&&"void"!==t||"hidden"===i)&&this._completeExit(),"visible"===i){const e=this._onEnter;this._ngZone.run(()=>{e.next(),e.complete()})}}enter(){this._destroyed||(this._animationState="visible",this._changeDetectorRef.detectChanges(),this._screenReaderAnnounce())}exit(){return this._animationState="hidden",this._elementRef.nativeElement.setAttribute("mat-exit",""),clearTimeout(this._announceTimeoutId),this._onExit}ngOnDestroy(){this._destroyed=!0,this._completeExit()}_completeExit(){this._ngZone.onMicrotaskEmpty.pipe(Ap(1)).subscribe(()=>{this._onExit.next(),this._onExit.complete()})}_applySnackBarClasses(){const e=this._elementRef.nativeElement,t=this.snackBarConfig.panelClass;t&&(Array.isArray(t)?t.forEach(t=>e.classList.add(t)):e.classList.add(t)),"center"===this.snackBarConfig.horizontalPosition&&e.classList.add("mat-snack-bar-center"),"top"===this.snackBarConfig.verticalPosition&&e.classList.add("mat-snack-bar-top")}_assertNotAttached(){this._portalOutlet.hasAttached()}_screenReaderAnnounce(){this._announceTimeoutId||this._ngZone.runOutsideAngular(()=>{this._announceTimeoutId=setTimeout(()=>{const e=this._elementRef.nativeElement.querySelector("[aria-hidden]"),t=this._elementRef.nativeElement.querySelector("[aria-live]");if(e&&t){let i=null;this._platform.isBrowser&&document.activeElement instanceof HTMLElement&&e.contains(document.activeElement)&&(i=document.activeElement),e.removeAttribute("aria-hidden"),t.appendChild(e),null==i||i.focus(),this._onAnnounce.next(),this._onAnnounce.complete()}},this._announceDelay)})}}return e.\u0275fac=function(t){return new(t||e)(xo(Tc),xo(ja),xo(hl),xo(rm),xo(qv))},e.\u0275cmp=$e({type:e,selectors:[["snack-bar-container"]],viewQuery:function(e,t){if(1&e&&ec(Tm,3),2&e){let e;Jl(e=ic())&&(t._portalOutlet=e.first)}},hostAttrs:[1,"mat-snack-bar-container"],hostVars:1,hostBindings:function(e,t){1&e&&Bo("@state.done",(function(e){return t.onAnimationEnd(e)})),2&e&&ma("@state",t._animationState)},features:[lo],decls:3,vars:2,consts:[["aria-hidden","true"],["cdkPortalOutlet",""]],template:function(e,t){1&e&&(To(0,"div",0),So(1,Vv,0,0,"ng-template",1),Oo(),Ro(2,"div")),2&e&&(Zr(2),Co("aria-live",t._live)("role",t._role))},directives:[Tm],styles:[".mat-snack-bar-container{border-radius:4px;box-sizing:border-box;display:block;margin:24px;max-width:33vw;min-width:344px;padding:14px 16px;min-height:48px;transform-origin:center}.cdk-high-contrast-active .mat-snack-bar-container{border:solid 1px}.mat-snack-bar-handset{width:100%}.mat-snack-bar-handset .mat-snack-bar-container{margin:8px;max-width:100%;min-width:0;width:100%}\n"],encapsulation:2,data:{animation:[Kv.snackBarState]}}),e})(),Zv=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[fg,Om,Wh,Av,qg],qg]}),e})();const Yv=new Qi("mat-snack-bar-default-options",{providedIn:"root",factory:function(){return new qv}});let Xv=(()=>{class e{constructor(e,t,i,n,r,s){this._overlay=e,this._live=t,this._injector=i,this._breakpointObserver=n,this._parentSnackBar=r,this._defaultConfig=s,this._snackBarRefAtThisLevel=null,this.simpleSnackBarComponent=$v,this.snackBarContainerComponent=Gv,this.handsetCssClass="mat-snack-bar-handset"}get _openedSnackBarRef(){const e=this._parentSnackBar;return e?e._openedSnackBarRef:this._snackBarRefAtThisLevel}set _openedSnackBarRef(e){this._parentSnackBar?this._parentSnackBar._openedSnackBarRef=e:this._snackBarRefAtThisLevel=e}openFromComponent(e,t){return this._attach(e,t)}openFromTemplate(e,t){return this._attach(e,t)}open(e,t="",i){const n=Object.assign(Object.assign({},this._defaultConfig),i);return n.data={message:e,action:t},n.announcementMessage===e&&(n.announcementMessage=void 0),this.openFromComponent(this.simpleSnackBarComponent,n)}dismiss(){this._openedSnackBarRef&&this._openedSnackBarRef.dismiss()}ngOnDestroy(){this._snackBarRefAtThisLevel&&this._snackBarRefAtThisLevel.dismiss()}_attachSnackBarContainer(e,t){const i=oo.create({parent:t&&t.viewContainerRef&&t.viewContainerRef.injector||this._injector,providers:[{provide:qv,useValue:t}]}),n=new Sm(this.snackBarContainerComponent,t.viewContainerRef,i),r=e.attach(n);return r.instance.snackBarConfig=t,r.instance}_attach(e,t){const i=Object.assign(Object.assign(Object.assign({},new qv),this._defaultConfig),t),n=this._createOverlay(i),r=this._attachSnackBarContainer(n,i),s=new Wv(r,n);if(e instanceof ml){const t=new km(e,null,{$implicit:i.data,snackBarRef:s});s.instance=r.attachTemplatePortal(t)}else{const t=this._createInjector(i,s),n=new Sm(e,void 0,t),o=r.attachComponentPortal(n);s.instance=o.instance}return this._breakpointObserver.observe("(max-width: 599.98px) and (orientation: portrait)").pipe(z_(n.detachments())).subscribe(e=>{const t=n.overlayElement.classList;e.matches?t.add(this.handsetCssClass):t.remove(this.handsetCssClass)}),i.announcementMessage&&r._onAnnounce.subscribe(()=>{this._live.announce(i.announcementMessage,i.politeness)}),this._animateSnackBar(s,i),this._openedSnackBarRef=s,this._openedSnackBarRef}_animateSnackBar(e,t){e.afterDismissed().subscribe(()=>{this._openedSnackBarRef==e&&(this._openedSnackBarRef=null),t.announcementMessage&&this._live.clear()}),this._openedSnackBarRef?(this._openedSnackBarRef.afterDismissed().subscribe(()=>{e.containerInstance.enter()}),this._openedSnackBarRef.dismiss()):e.containerInstance.enter(),t.duration&&t.duration>0&&e.afterOpened().subscribe(()=>e._dismissAfter(t.duration))}_createOverlay(e){const t=new Vm;t.direction=e.direction;let i=this._overlay.position().global();const n="rtl"===e.direction,r="left"===e.horizontalPosition||"start"===e.horizontalPosition&&!n||"end"===e.horizontalPosition&&n,s=!r&&"center"!==e.horizontalPosition;return r?i.left("0"):s?i.right("0"):i.centerHorizontally(),"top"===e.verticalPosition?i.top("0"):i.bottom("0"),t.positionStrategy=i,this._overlay.create(t)}_createInjector(e,t){return oo.create({parent:e&&e.viewContainerRef&&e.viewContainerRef.injector||this._injector,providers:[{provide:Wv,useValue:t},{provide:Uv,useValue:e.data}]})}}return e.\u0275fac=function(t){return new(t||e)(mn(ag),mn(xg),mn(oo),mn(Bv),mn(e,12),mn(Yv))},e.\u0275prov=pe({factory:function(){return new e(mn(ag),mn(xg),mn(zs),mn(Bv),mn(e,12),mn(Yv))},token:e,providedIn:Zv}),e})();class Qv extends S{constructor(e){super(),this._value=e}get value(){return this.getValue()}_subscribe(e){const t=super._subscribe(e);return t&&!t.closed&&e.next(this._value),t}getValue(){if(this.hasError)throw this.thrownError;if(this.closed)throw new b;return this._value}next(e){super.next(this._value=e)}}const Jv=(()=>{function e(){return Error.call(this),this.message="no elements in sequence",this.name="EmptyError",this}return e.prototype=Object.create(Error.prototype),e})();function ey(e,t){let i=!1;return arguments.length>=2&&(i=!0),function(n){return n.lift(new ty(e,t,i))}}class ty{constructor(e,t,i=!1){this.accumulator=e,this.seed=t,this.hasSeed=i}call(e,t){return t.subscribe(new iy(e,this.accumulator,this.seed,this.hasSeed))}}class iy extends p{constructor(e,t,i,n){super(e),this.accumulator=t,this._seed=i,this.hasSeed=n,this.index=0}get seed(){return this._seed}set seed(e){this.hasSeed=!0,this._seed=e}_next(e){if(this.hasSeed)return this._tryNext(e);this.seed=e,this.destination.next(e)}_tryNext(e){const t=this.index++;let i;try{i=this.accumulator(this.seed,e,t)}catch(n){this.destination.error(n)}this.seed=i,this.destination.next(i)}}function ny(e){return function(t){const i=new ry(e),n=t.lift(i);return i.caught=n}}class ry{constructor(e){this.selector=e}call(e,t){return t.subscribe(new sy(e,this.selector,this.caught))}}class sy extends I{constructor(e,t,i){super(e),this.selector=t,this.caught=i}error(e){if(!this.isStopped){let i;try{i=this.selector(e,this.caught)}catch(t){return void super.error(t)}this._unsubscribeAndRecycle();const n=new E(this,void 0,void 0);this.add(n);const r=P(this,i,void 0,void 0,n);r!==n&&this.add(r)}}}function oy(e){return function(t){return 0===e?up():t.lift(new ay(e))}}class ay{constructor(e){if(this.total=e,this.total<0)throw new Ep}call(e,t){return t.subscribe(new ly(e,this.total))}}class ly extends p{constructor(e,t){super(e),this.total=t,this.ring=new Array,this.count=0}_next(e){const t=this.ring,i=this.total,n=this.count++;t.length0){const i=this.count>=this.total?this.total:this.count,n=this.ring;for(let r=0;rt.lift(new hy(e))}class hy{constructor(e){this.errorFactory=e}call(e,t){return t.subscribe(new uy(e,this.errorFactory))}}class uy extends p{constructor(e,t){super(e),this.errorFactory=t,this.hasValue=!1}_next(e){this.hasValue=!0,this.destination.next(e)}_complete(){if(this.hasValue)return this.destination.complete();{let t;try{t=this.errorFactory()}catch(e){t=e}this.destination.error(t)}}}function dy(){return new Jv}function fy(e=null){return t=>t.lift(new py(e))}class py{constructor(e){this.defaultValue=e}call(e,t){return t.subscribe(new _y(e,this.defaultValue))}}class _y extends p{constructor(e,t){super(e),this.defaultValue=t,this.isEmpty=!0}_next(e){this.isEmpty=!1,this.destination.next(e)}_complete(){this.isEmpty&&this.destination.next(this.defaultValue),this.destination.complete()}}function my(e,t){const i=arguments.length>=2;return n=>n.pipe(e?bp((t,i)=>e(t,i,n)):g,Ap(1),i?fy(t):cy(()=>new Jv))}class gy{constructor(e){this.callback=e}call(e,t){return t.subscribe(new vy(e,this.callback))}}class vy extends p{constructor(e,t){super(e),this.add(new u(t))}}class yy{constructor(e,t){this.id=e,this.url=t}}class by extends yy{constructor(e,t,i="imperative",n=null){super(e,t),this.navigationTrigger=i,this.restoredState=n}toString(){return`NavigationStart(id: ${this.id}, url: '${this.url}')`}}class Cy extends yy{constructor(e,t,i){super(e,t),this.urlAfterRedirects=i}toString(){return`NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`}}class wy extends yy{constructor(e,t,i){super(e,t),this.reason=i}toString(){return`NavigationCancel(id: ${this.id}, url: '${this.url}')`}}class Sy extends yy{constructor(e,t,i){super(e,t),this.error=i}toString(){return`NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`}}class ky extends yy{constructor(e,t,i,n){super(e,t),this.urlAfterRedirects=i,this.state=n}toString(){return`RoutesRecognized(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class xy extends yy{constructor(e,t,i,n){super(e,t),this.urlAfterRedirects=i,this.state=n}toString(){return`GuardsCheckStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class Ey extends yy{constructor(e,t,i,n,r){super(e,t),this.urlAfterRedirects=i,this.state=n,this.shouldActivate=r}toString(){return`GuardsCheckEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state}, shouldActivate: ${this.shouldActivate})`}}class Ay extends yy{constructor(e,t,i,n){super(e,t),this.urlAfterRedirects=i,this.state=n}toString(){return`ResolveStart(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class Ty extends yy{constructor(e,t,i,n){super(e,t),this.urlAfterRedirects=i,this.state=n}toString(){return`ResolveEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}', state: ${this.state})`}}class Oy{constructor(e){this.route=e}toString(){return`RouteConfigLoadStart(path: ${this.route.path})`}}class Ry{constructor(e){this.route=e}toString(){return`RouteConfigLoadEnd(path: ${this.route.path})`}}class Ly{constructor(e){this.snapshot=e}toString(){return`ChildActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class Dy{constructor(e){this.snapshot=e}toString(){return`ChildActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class Py{constructor(e){this.snapshot=e}toString(){return`ActivationStart(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class Iy{constructor(e){this.snapshot=e}toString(){return`ActivationEnd(path: '${this.snapshot.routeConfig&&this.snapshot.routeConfig.path||""}')`}}class My{constructor(e,t,i){this.routerEvent=e,this.position=t,this.anchor=i}toString(){return`Scroll(anchor: '${this.anchor}', position: '${this.position?`${this.position[0]}, ${this.position[1]}`:null}')`}}const Fy="primary";class Hy{constructor(e){this.params=e||{}}has(e){return Object.prototype.hasOwnProperty.call(this.params,e)}get(e){if(this.has(e)){const t=this.params[e];return Array.isArray(t)?t[0]:t}return null}getAll(e){if(this.has(e)){const t=this.params[e];return Array.isArray(t)?t:[t]}return[]}get keys(){return Object.keys(this.params)}}function By(e){return new Hy(e)}function jy(e){const t=Error("NavigationCancelingError: "+e);return t.ngNavigationCancelingError=!0,t}function Ny(e,t,i){const n=i.path.split("/");if(n.length>e.length)return null;if("full"===i.pathMatch&&(t.hasChildren()||n.lengthn[t]===e)}return e===t}function qy(e){return Array.prototype.concat.apply([],e)}function zy(e){return e.length>0?e[e.length-1]:null}function Wy(e,t){for(const i in e)e.hasOwnProperty(i)&&t(e[i],i)}function $y(e){return Fo(e)?e:Io(e)?j(Promise.resolve(e)):mp(e)}function Ky(e,t,i){return i?function(e,t){return Vy(e,t)}(e.queryParams,t.queryParams)&&function e(t,i){if(!Xy(t.segments,i.segments))return!1;if(t.numberOfChildren!==i.numberOfChildren)return!1;for(const n in i.children){if(!t.children[n])return!1;if(!e(t.children[n],i.children[n]))return!1}return!0}(e.root,t.root):function(e,t){return Object.keys(t).length<=Object.keys(e).length&&Object.keys(t).every(i=>Uy(e[i],t[i]))}(e.queryParams,t.queryParams)&&function e(t,i){return function t(i,n,r){if(i.segments.length>r.length)return!!Xy(i.segments.slice(0,r.length),r)&&!n.hasChildren();if(i.segments.length===r.length){if(!Xy(i.segments,r))return!1;for(const t in n.children){if(!i.children[t])return!1;if(!e(i.children[t],n.children[t]))return!1}return!0}{const e=r.slice(0,i.segments.length),s=r.slice(i.segments.length);return!!Xy(i.segments,e)&&!!i.children.primary&&t(i.children.primary,n,s)}}(t,i,i.segments)}(e.root,t.root)}class Gy{constructor(e,t,i){this.root=e,this.queryParams=t,this.fragment=i}get queryParamMap(){return this._queryParamMap||(this._queryParamMap=By(this.queryParams)),this._queryParamMap}toString(){return eb.serialize(this)}}class Zy{constructor(e,t){this.segments=e,this.children=t,this.parent=null,Wy(t,(e,t)=>e.parent=this)}hasChildren(){return this.numberOfChildren>0}get numberOfChildren(){return Object.keys(this.children).length}toString(){return tb(this)}}class Yy{constructor(e,t){this.path=e,this.parameters=t}get parameterMap(){return this._parameterMap||(this._parameterMap=By(this.parameters)),this._parameterMap}toString(){return ab(this)}}function Xy(e,t){return e.length===t.length&&e.every((e,i)=>e.path===t[i].path)}class Qy{}class Jy{parse(e){const t=new db(e);return new Gy(t.parseRootSegment(),t.parseQueryParams(),t.parseFragment())}serialize(e){return`${"/"+function e(t,i){if(!t.hasChildren())return tb(t);if(i){const i=t.children.primary?e(t.children.primary,!1):"",n=[];return Wy(t.children,(t,i)=>{i!==Fy&&n.push(`${i}:${e(t,!1)}`)}),n.length>0?`${i}(${n.join("//")})`:i}{const i=function(e,t){let i=[];return Wy(e.children,(e,n)=>{n===Fy&&(i=i.concat(t(e,n)))}),Wy(e.children,(e,n)=>{n!==Fy&&(i=i.concat(t(e,n)))}),i}(t,(i,n)=>n===Fy?[e(t.children.primary,!1)]:[`${n}:${e(i,!1)}`]);return 1===Object.keys(t.children).length&&null!=t.children.primary?`${tb(t)}/${i[0]}`:`${tb(t)}/(${i.join("//")})`}}(e.root,!0)}${function(e){const t=Object.keys(e).map(t=>{const i=e[t];return Array.isArray(i)?i.map(e=>`${nb(t)}=${nb(e)}`).join("&"):`${nb(t)}=${nb(i)}`});return t.length?"?"+t.join("&"):""}(e.queryParams)}${"string"==typeof e.fragment?"#"+encodeURI(e.fragment):""}`}}const eb=new Jy;function tb(e){return e.segments.map(e=>ab(e)).join("/")}function ib(e){return encodeURIComponent(e).replace(/%40/g,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",")}function nb(e){return ib(e).replace(/%3B/gi,";")}function rb(e){return ib(e).replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/%26/gi,"&")}function sb(e){return decodeURIComponent(e)}function ob(e){return sb(e.replace(/\+/g,"%20"))}function ab(e){return`${rb(e.path)}${t=e.parameters,Object.keys(t).map(e=>`;${rb(e)}=${rb(t[e])}`).join("")}`;var t}const lb=/^[^\/()?;=#]+/;function cb(e){const t=e.match(lb);return t?t[0]:""}const hb=/^[^=?]+/,ub=/^[^?]+/;class db{constructor(e){this.url=e,this.remaining=e}parseRootSegment(){return this.consumeOptional("/"),""===this.remaining||this.peekStartsWith("?")||this.peekStartsWith("#")?new Zy([],{}):new Zy([],this.parseChildren())}parseQueryParams(){const e={};if(this.consumeOptional("?"))do{this.parseQueryParam(e)}while(this.consumeOptional("&"));return e}parseFragment(){return this.consumeOptional("#")?decodeURIComponent(this.remaining):null}parseChildren(){if(""===this.remaining)return{};this.consumeOptional("/");const e=[];for(this.peekStartsWith("(")||e.push(this.parseSegment());this.peekStartsWith("/")&&!this.peekStartsWith("//")&&!this.peekStartsWith("/(");)this.capture("/"),e.push(this.parseSegment());let t={};this.peekStartsWith("/(")&&(this.capture("/"),t=this.parseParens(!0));let i={};return this.peekStartsWith("(")&&(i=this.parseParens(!1)),(e.length>0||Object.keys(t).length>0)&&(i.primary=new Zy(e,t)),i}parseSegment(){const e=cb(this.remaining);if(""===e&&this.peekStartsWith(";"))throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`);return this.capture(e),new Yy(sb(e),this.parseMatrixParams())}parseMatrixParams(){const e={};for(;this.consumeOptional(";");)this.parseParam(e);return e}parseParam(e){const t=cb(this.remaining);if(!t)return;this.capture(t);let i="";if(this.consumeOptional("=")){const e=cb(this.remaining);e&&(i=e,this.capture(i))}e[sb(t)]=sb(i)}parseQueryParam(e){const t=function(e){const t=e.match(hb);return t?t[0]:""}(this.remaining);if(!t)return;this.capture(t);let i="";if(this.consumeOptional("=")){const e=function(e){const t=e.match(ub);return t?t[0]:""}(this.remaining);e&&(i=e,this.capture(i))}const n=ob(t),r=ob(i);if(e.hasOwnProperty(n)){let t=e[n];Array.isArray(t)||(t=[t],e[n]=t),t.push(r)}else e[n]=r}parseParens(e){const t={};for(this.capture("(");!this.consumeOptional(")")&&this.remaining.length>0;){const i=cb(this.remaining),n=this.remaining[i.length];if("/"!==n&&")"!==n&&";"!==n)throw new Error(`Cannot parse url '${this.url}'`);let r=void 0;i.indexOf(":")>-1?(r=i.substr(0,i.indexOf(":")),this.capture(r),this.capture(":")):e&&(r=Fy);const s=this.parseChildren();t[r]=1===Object.keys(s).length?s.primary:new Zy([],s),this.consumeOptional("//")}return t}peekStartsWith(e){return this.remaining.startsWith(e)}consumeOptional(e){return!!this.peekStartsWith(e)&&(this.remaining=this.remaining.substring(e.length),!0)}capture(e){if(!this.consumeOptional(e))throw new Error(`Expected "${e}".`)}}class fb{constructor(e){this._root=e}get root(){return this._root.value}parent(e){const t=this.pathFromRoot(e);return t.length>1?t[t.length-2]:null}children(e){const t=pb(e,this._root);return t?t.children.map(e=>e.value):[]}firstChild(e){const t=pb(e,this._root);return t&&t.children.length>0?t.children[0].value:null}siblings(e){const t=_b(e,this._root);return t.length<2?[]:t[t.length-2].children.map(e=>e.value).filter(t=>t!==e)}pathFromRoot(e){return _b(e,this._root).map(e=>e.value)}}function pb(e,t){if(e===t.value)return t;for(const i of t.children){const t=pb(e,i);if(t)return t}return null}function _b(e,t){if(e===t.value)return[t];for(const i of t.children){const n=_b(e,i);if(n.length)return n.unshift(t),n}return[]}class mb{constructor(e,t){this.value=e,this.children=t}toString(){return`TreeNode(${this.value})`}}function gb(e){const t={};return e&&e.children.forEach(e=>t[e.value.outlet]=e),t}class vb extends fb{constructor(e,t){super(e),this.snapshot=t,kb(this,e)}toString(){return this.snapshot.toString()}}function yb(e,t){const i=function(e,t){const i=new wb([],{},{},"",{},Fy,t,null,e.root,-1,{});return new Sb("",new mb(i,[]))}(e,t),n=new Qv([new Yy("",{})]),r=new Qv({}),s=new Qv({}),o=new Qv({}),a=new Qv(""),l=new bb(n,r,o,a,s,Fy,t,i.root);return l.snapshot=i.root,new vb(new mb(l,[]),i)}class bb{constructor(e,t,i,n,r,s,o,a){this.url=e,this.params=t,this.queryParams=i,this.fragment=n,this.data=r,this.outlet=s,this.component=o,this._futureSnapshot=a}get routeConfig(){return this._futureSnapshot.routeConfig}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap||(this._paramMap=this.params.pipe(M(e=>By(e)))),this._paramMap}get queryParamMap(){return this._queryParamMap||(this._queryParamMap=this.queryParams.pipe(M(e=>By(e)))),this._queryParamMap}toString(){return this.snapshot?this.snapshot.toString():`Future(${this._futureSnapshot})`}}function Cb(e,t="emptyOnly"){const i=e.pathFromRoot;let n=0;if("always"!==t)for(n=i.length-1;n>=1;){const e=i[n],t=i[n-1];if(e.routeConfig&&""===e.routeConfig.path)n--;else{if(t.component)break;n--}}return function(e){return e.reduce((e,t)=>({params:Object.assign(Object.assign({},e.params),t.params),data:Object.assign(Object.assign({},e.data),t.data),resolve:Object.assign(Object.assign({},e.resolve),t._resolvedData)}),{params:{},data:{},resolve:{}})}(i.slice(n))}class wb{constructor(e,t,i,n,r,s,o,a,l,c,h){this.url=e,this.params=t,this.queryParams=i,this.fragment=n,this.data=r,this.outlet=s,this.component=o,this.routeConfig=a,this._urlSegment=l,this._lastPathIndex=c,this._resolve=h}get root(){return this._routerState.root}get parent(){return this._routerState.parent(this)}get firstChild(){return this._routerState.firstChild(this)}get children(){return this._routerState.children(this)}get pathFromRoot(){return this._routerState.pathFromRoot(this)}get paramMap(){return this._paramMap||(this._paramMap=By(this.params)),this._paramMap}get queryParamMap(){return this._queryParamMap||(this._queryParamMap=By(this.queryParams)),this._queryParamMap}toString(){return`Route(url:'${this.url.map(e=>e.toString()).join("/")}', path:'${this.routeConfig?this.routeConfig.path:""}')`}}class Sb extends fb{constructor(e,t){super(t),this.url=e,kb(this,t)}toString(){return xb(this._root)}}function kb(e,t){t.value._routerState=e,t.children.forEach(t=>kb(e,t))}function xb(e){const t=e.children.length>0?` { ${e.children.map(xb).join(", ")} } `:"";return`${e.value}${t}`}function Eb(e){if(e.snapshot){const t=e.snapshot,i=e._futureSnapshot;e.snapshot=i,Vy(t.queryParams,i.queryParams)||e.queryParams.next(i.queryParams),t.fragment!==i.fragment&&e.fragment.next(i.fragment),Vy(t.params,i.params)||e.params.next(i.params),function(e,t){if(e.length!==t.length)return!1;for(let i=0;iVy(e.parameters,n[t].parameters))&&!(!e.parent!=!t.parent)&&(!e.parent||Ab(e.parent,t.parent))}function Tb(e){return"object"==typeof e&&null!=e&&!e.outlets&&!e.segmentPath}function Ob(e){return"object"==typeof e&&null!=e&&e.outlets}function Rb(e,t,i,n,r){let s={};return n&&Wy(n,(e,t)=>{s[t]=Array.isArray(e)?e.map(e=>""+e):""+e}),new Gy(i.root===e?t:function e(t,i,n){const r={};return Wy(t.children,(t,s)=>{r[s]=t===i?n:e(t,i,n)}),new Zy(t.segments,r)}(i.root,e,t),s,r)}class Lb{constructor(e,t,i){if(this.isAbsolute=e,this.numberOfDoubleDots=t,this.commands=i,e&&i.length>0&&Tb(i[0]))throw new Error("Root segment cannot have matrix parameters");const n=i.find(Ob);if(n&&n!==zy(i))throw new Error("{outlets:{}} has to be the last command")}toRoot(){return this.isAbsolute&&1===this.commands.length&&"/"==this.commands[0]}}class Db{constructor(e,t,i){this.segmentGroup=e,this.processChildren=t,this.index=i}}function Pb(e,t,i){if(e||(e=new Zy([],{})),0===e.segments.length&&e.hasChildren())return Ib(e,t,i);const n=function(e,t,i){let n=0,r=t;const s={match:!1,pathIndex:0,commandIndex:0};for(;r=i.length)return s;const t=e.segments[r],o=i[n];if(Ob(o))break;const a=""+o,l=n0&&void 0===a)break;if(a&&l&&"object"==typeof l&&void 0===l.outlets){if(!Bb(a,l,t))return s;n+=2}else{if(!Bb(a,{},t))return s;n++}r++}return{match:!0,pathIndex:r,commandIndex:n}}(e,t,i),r=i.slice(n.commandIndex);if(n.match&&n.pathIndex{"string"==typeof i&&(i=[i]),null!==i&&(r[n]=Pb(e.children[n],t,i))}),Wy(e.children,(e,t)=>{void 0===n[t]&&(r[t]=e)}),new Zy(e.segments,r)}}function Mb(e,t,i){const n=e.segments.slice(0,t);let r=0;for(;r{"string"==typeof e&&(e=[e]),null!==e&&(t[i]=Mb(new Zy([],{}),0,e))}),t}function Hb(e){const t={};return Wy(e,(e,i)=>t[i]=""+e),t}function Bb(e,t,i){return e==i.path&&Vy(t,i.parameters)}class jb{constructor(e,t,i,n){this.routeReuseStrategy=e,this.futureState=t,this.currState=i,this.forwardEvent=n}activate(e){const t=this.futureState._root,i=this.currState?this.currState._root:null;this.deactivateChildRoutes(t,i,e),Eb(this.futureState.root),this.activateChildRoutes(t,i,e)}deactivateChildRoutes(e,t,i){const n=gb(t);e.children.forEach(e=>{const t=e.value.outlet;this.deactivateRoutes(e,n[t],i),delete n[t]}),Wy(n,(e,t)=>{this.deactivateRouteAndItsChildren(e,i)})}deactivateRoutes(e,t,i){const n=e.value,r=t?t.value:null;if(n===r)if(n.component){const r=i.getContext(n.outlet);r&&this.deactivateChildRoutes(e,t,r.children)}else this.deactivateChildRoutes(e,t,i);else r&&this.deactivateRouteAndItsChildren(t,i)}deactivateRouteAndItsChildren(e,t){this.routeReuseStrategy.shouldDetach(e.value.snapshot)?this.detachAndStoreRouteSubtree(e,t):this.deactivateRouteAndOutlet(e,t)}detachAndStoreRouteSubtree(e,t){const i=t.getContext(e.value.outlet);if(i&&i.outlet){const t=i.outlet.detach(),n=i.children.onOutletDeactivated();this.routeReuseStrategy.store(e.value.snapshot,{componentRef:t,route:e,contexts:n})}}deactivateRouteAndOutlet(e,t){const i=t.getContext(e.value.outlet),n=i&&e.value.component?i.children:t,r=gb(e);for(const s of Object.keys(r))this.deactivateRouteAndItsChildren(r[s],n);i&&i.outlet&&(i.outlet.deactivate(),i.children.onOutletDeactivated())}activateChildRoutes(e,t,i){const n=gb(t);e.children.forEach(e=>{this.activateRoutes(e,n[e.value.outlet],i),this.forwardEvent(new Iy(e.value.snapshot))}),e.children.length&&this.forwardEvent(new Dy(e.value.snapshot))}activateRoutes(e,t,i){const n=e.value,r=t?t.value:null;if(Eb(n),n===r)if(n.component){const r=i.getOrCreateContext(n.outlet);this.activateChildRoutes(e,t,r.children)}else this.activateChildRoutes(e,t,i);else if(n.component){const t=i.getOrCreateContext(n.outlet);if(this.routeReuseStrategy.shouldAttach(n.snapshot)){const e=this.routeReuseStrategy.retrieve(n.snapshot);this.routeReuseStrategy.store(n.snapshot,null),t.children.onOutletReAttached(e.contexts),t.attachRef=e.componentRef,t.route=e.route.value,t.outlet&&t.outlet.attach(e.componentRef,e.route.value),Nb(e.route)}else{const i=function(e){for(let t=e.parent;t;t=t.parent){const e=t.routeConfig;if(e&&e._loadedConfig)return e._loadedConfig;if(e&&e.component)return null}return null}(n.snapshot),r=i?i.module.componentFactoryResolver:null;t.attachRef=null,t.route=n,t.resolver=r,t.outlet&&t.outlet.activateWith(n,r),this.activateChildRoutes(e,null,t.children)}}else this.activateChildRoutes(e,null,i)}}function Nb(e){Eb(e.value),e.children.forEach(Nb)}class Vb{constructor(e,t){this.routes=e,this.module=t}}function Ub(e){return"function"==typeof e}function qb(e){return e instanceof Gy}const zb=Symbol("INITIAL_VALUE");function Wb(){return Sp(e=>Ov(e.map(e=>e.pipe(Ap(1),K_(zb)))).pipe(ey((e,t)=>{let i=!1;return t.reduce((e,n,r)=>{if(e!==zb)return e;if(n===zb&&(i=!0),!i){if(!1===n)return n;if(r===t.length-1||qb(n))return n}return e},e)},zb),bp(e=>e!==zb),M(e=>qb(e)?e:!0===e),Ap(1)))}let $b=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275cmp=$e({type:e,selectors:[["ng-component"]],decls:1,vars:0,template:function(e,t){1&e&&Ro(0,"router-outlet")},directives:function(){return[NC]},encapsulation:2}),e})();function Kb(e,t=""){for(let i=0;iXb(e)===t);return i.push(...e.filter(e=>Xb(e)!==t)),i}const Jb={matched:!1,consumedSegments:[],lastChild:0,parameters:{},positionalParamSegments:{}};function eC(e,t,i){var n;if(""===t.path)return"full"===t.pathMatch&&(e.hasChildren()||i.length>0)?Object.assign({},Jb):{matched:!0,consumedSegments:[],lastChild:0,parameters:{},positionalParamSegments:{}};const r=(t.matcher||Ny)(i,e,t);if(!r)return Object.assign({},Jb);const s={};Wy(r.posParams,(e,t)=>{s[t]=e.path});const o=r.consumed.length>0?Object.assign(Object.assign({},s),r.consumed[r.consumed.length-1].parameters):s;return{matched:!0,consumedSegments:r.consumed,lastChild:r.consumed.length,parameters:o,positionalParamSegments:null!==(n=r.posParams)&&void 0!==n?n:{}}}function tC(e,t,i,n,r="corrected"){if(i.length>0&&function(e,t,i){return i.some(i=>iC(e,t,i)&&Xb(i)!==Fy)}(e,i,n)){const r=new Zy(t,function(e,t,i,n){const r={};r.primary=n,n._sourceSegment=e,n._segmentIndexShift=t.length;for(const s of i)if(""===s.path&&Xb(s)!==Fy){const i=new Zy([],{});i._sourceSegment=e,i._segmentIndexShift=t.length,r[Xb(s)]=i}return r}(e,t,n,new Zy(i,e.children)));return r._sourceSegment=e,r._segmentIndexShift=t.length,{segmentGroup:r,slicedSegments:[]}}if(0===i.length&&function(e,t,i){return i.some(i=>iC(e,t,i))}(e,i,n)){const s=new Zy(e.segments,function(e,t,i,n,r,s){const o={};for(const a of n)if(iC(e,i,a)&&!r[Xb(a)]){const i=new Zy([],{});i._sourceSegment=e,i._segmentIndexShift="legacy"===s?e.segments.length:t.length,o[Xb(a)]=i}return Object.assign(Object.assign({},r),o)}(e,t,i,n,e.children,r));return s._sourceSegment=e,s._segmentIndexShift=t.length,{segmentGroup:s,slicedSegments:i}}const s=new Zy(e.segments,e.children);return s._sourceSegment=e,s._segmentIndexShift=t.length,{segmentGroup:s,slicedSegments:i}}function iC(e,t,i){return(!(e.hasChildren()||t.length>0)||"full"!==i.pathMatch)&&""===i.path}function nC(e,t,i,n){return!!(Xb(e)===n||n!==Fy&&iC(t,i,e))&&("**"===e.path||eC(t,e,i).matched)}function rC(e,t,i){return 0===t.length&&!e.children[i]}class sC{constructor(e){this.segmentGroup=e||null}}class oC{constructor(e){this.urlTree=e}}function aC(e){return new v(t=>t.error(new sC(e)))}function lC(e){return new v(t=>t.error(new oC(e)))}function cC(e){return new v(t=>t.error(new Error(`Only absolute redirects can have named outlets. redirectTo: '${e}'`)))}class hC{constructor(e,t,i,n,r){this.configLoader=t,this.urlSerializer=i,this.urlTree=n,this.config=r,this.allowRedirects=!0,this.ngModule=e.get(bl)}apply(){const e=tC(this.urlTree.root,[],[],this.config).segmentGroup,t=new Zy(e.segments,e.children);return this.expandSegmentGroup(this.ngModule,this.config,t,Fy).pipe(M(e=>this.createUrlTree(uC(e),this.urlTree.queryParams,this.urlTree.fragment))).pipe(ny(e=>{if(e instanceof oC)return this.allowRedirects=!1,this.match(e.urlTree);if(e instanceof sC)throw this.noMatchError(e);throw e}))}match(e){return this.expandSegmentGroup(this.ngModule,this.config,e.root,Fy).pipe(M(t=>this.createUrlTree(uC(t),e.queryParams,e.fragment))).pipe(ny(e=>{if(e instanceof sC)throw this.noMatchError(e);throw e}))}noMatchError(e){return new Error(`Cannot match any routes. URL Segment: '${e.segmentGroup}'`)}createUrlTree(e,t,i){const n=e.segments.length>0?new Zy([],{[Fy]:e}):e;return new Gy(n,t,i)}expandSegmentGroup(e,t,i,n){return 0===i.segments.length&&i.hasChildren()?this.expandChildren(e,t,i).pipe(M(e=>new Zy([],e))):this.expandSegment(e,i,t,i.segments,n,!0)}expandChildren(e,t,i){const n=[];for(const r of Object.keys(i.children))"primary"===r?n.unshift(r):n.push(r);return j(n).pipe(Jp(n=>{const r=i.children[n],s=Qb(t,n);return this.expandSegmentGroup(e,s,r,n).pipe(M(e=>({segment:e,outlet:n})))}),ey((e,t)=>(e[t.outlet]=t.segment,e),{}),function(e,t){const i=arguments.length>=2;return n=>n.pipe(e?bp((t,i)=>e(t,i,n)):g,oy(1),i?fy(t):cy(()=>new Jv))}())}expandSegment(e,t,i,n,r,s){return j(i).pipe(Jp(o=>this.expandSegmentAgainstRoute(e,t,i,o,n,r,s).pipe(ny(e=>{if(e instanceof sC)return mp(null);throw e}))),my(e=>!!e),ny((e,i)=>{if(e instanceof Jv||"EmptyError"===e.name){if(rC(t,n,r))return mp(new Zy([],{}));throw new sC(t)}throw e}))}expandSegmentAgainstRoute(e,t,i,n,r,s,o){return nC(n,t,r,s)?void 0===n.redirectTo?this.matchSegmentAgainstRoute(e,t,n,r,s):o&&this.allowRedirects?this.expandSegmentAgainstRouteUsingRedirect(e,t,i,n,r,s):aC(t):aC(t)}expandSegmentAgainstRouteUsingRedirect(e,t,i,n,r,s){return"**"===n.path?this.expandWildCardWithParamsAgainstRouteUsingRedirect(e,i,n,s):this.expandRegularSegmentAgainstRouteUsingRedirect(e,t,i,n,r,s)}expandWildCardWithParamsAgainstRouteUsingRedirect(e,t,i,n){const r=this.applyRedirectCommands([],i.redirectTo,{});return i.redirectTo.startsWith("/")?lC(r):this.lineralizeSegments(i,r).pipe(N(i=>{const r=new Zy(i,{});return this.expandSegment(e,r,t,i,n,!1)}))}expandRegularSegmentAgainstRouteUsingRedirect(e,t,i,n,r,s){const{matched:o,consumedSegments:a,lastChild:l,positionalParamSegments:c}=eC(t,n,r);if(!o)return aC(t);const h=this.applyRedirectCommands(a,n.redirectTo,c);return n.redirectTo.startsWith("/")?lC(h):this.lineralizeSegments(n,h).pipe(N(n=>this.expandSegment(e,t,i,n.concat(r.slice(l)),s,!1)))}matchSegmentAgainstRoute(e,t,i,n,r){if("**"===i.path)return i.loadChildren?(i._loadedConfig?mp(i._loadedConfig):this.configLoader.load(e.injector,i)).pipe(M(e=>(i._loadedConfig=e,new Zy(n,{})))):mp(new Zy(n,{}));const{matched:s,consumedSegments:o,lastChild:a}=eC(t,i,n);if(!s)return aC(t);const l=n.slice(a);return this.getChildConfig(e,i,n).pipe(N(e=>{const n=e.module,s=e.routes,{segmentGroup:a,slicedSegments:c}=tC(t,o,l,s),h=new Zy(a.segments,a.children);if(0===c.length&&h.hasChildren())return this.expandChildren(n,s,h).pipe(M(e=>new Zy(o,e)));if(0===s.length&&0===c.length)return mp(new Zy(o,{}));const u=Xb(i)===r;return this.expandSegment(n,h,s,c,u?Fy:r,!0).pipe(M(e=>new Zy(o.concat(e.segments),e.children)))}))}getChildConfig(e,t,i){return t.children?mp(new Vb(t.children,e)):t.loadChildren?void 0!==t._loadedConfig?mp(t._loadedConfig):this.runCanLoadGuards(e.injector,t,i).pipe(N(i=>i?this.configLoader.load(e.injector,t).pipe(M(e=>(t._loadedConfig=e,e))):function(e){return new v(t=>t.error(jy(`Cannot load children because the guard of the route "path: '${e.path}'" returned false`)))}(t))):mp(new Vb([],e))}runCanLoadGuards(e,t,i){const n=t.canLoad;return n&&0!==n.length?mp(n.map(n=>{const r=e.get(n);let s;if(function(e){return e&&Ub(e.canLoad)}(r))s=r.canLoad(t,i);else{if(!Ub(r))throw new Error("Invalid CanLoad guard");s=r(t,i)}return $y(s)})).pipe(Wb(),Rp(e=>{if(!qb(e))return;const t=jy(`Redirecting to "${this.urlSerializer.serialize(e)}"`);throw t.url=e,t}),M(e=>!0===e)):mp(!0)}lineralizeSegments(e,t){let i=[],n=t.root;for(;;){if(i=i.concat(n.segments),0===n.numberOfChildren)return mp(i);if(n.numberOfChildren>1||!n.children.primary)return cC(e.redirectTo);n=n.children.primary}}applyRedirectCommands(e,t,i){return this.applyRedirectCreatreUrlTree(t,this.urlSerializer.parse(t),e,i)}applyRedirectCreatreUrlTree(e,t,i,n){const r=this.createSegmentGroup(e,t.root,i,n);return new Gy(r,this.createQueryParams(t.queryParams,this.urlTree.queryParams),t.fragment)}createQueryParams(e,t){const i={};return Wy(e,(e,n)=>{if("string"==typeof e&&e.startsWith(":")){const r=e.substring(1);i[n]=t[r]}else i[n]=e}),i}createSegmentGroup(e,t,i,n){const r=this.createSegments(e,t.segments,i,n);let s={};return Wy(t.children,(t,r)=>{s[r]=this.createSegmentGroup(e,t,i,n)}),new Zy(r,s)}createSegments(e,t,i,n){return t.map(t=>t.path.startsWith(":")?this.findPosParam(e,t,n):this.findOrReturn(t,i))}findPosParam(e,t,i){const n=i[t.path.substring(1)];if(!n)throw new Error(`Cannot redirect to '${e}'. Cannot find '${t.path}'.`);return n}findOrReturn(e,t){let i=0;for(const n of t){if(n.path===e.path)return t.splice(i),n;i++}return e}}function uC(e){const t={};for(const i of Object.keys(e.children)){const n=uC(e.children[i]);(n.segments.length>0||n.hasChildren())&&(t[i]=n)}return function(e){if(1===e.numberOfChildren&&e.children.primary){const t=e.children.primary;return new Zy(e.segments.concat(t.segments),t.children)}return e}(new Zy(e.segments,t))}class dC{constructor(e){this.path=e,this.route=this.path[this.path.length-1]}}class fC{constructor(e,t){this.component=e,this.route=t}}function pC(e,t,i){const n=e._root;return function e(t,i,n,r,s={canDeactivateChecks:[],canActivateChecks:[]}){const o=gb(i);return t.children.forEach(t=>{!function(t,i,n,r,s={canDeactivateChecks:[],canActivateChecks:[]}){const o=t.value,a=i?i.value:null,l=n?n.getContext(t.value.outlet):null;if(a&&o.routeConfig===a.routeConfig){const c=function(e,t,i){if("function"==typeof i)return i(e,t);switch(i){case"pathParamsChange":return!Xy(e.url,t.url);case"pathParamsOrQueryParamsChange":return!Xy(e.url,t.url)||!Vy(e.queryParams,t.queryParams);case"always":return!0;case"paramsOrQueryParamsChange":return!Ab(e,t)||!Vy(e.queryParams,t.queryParams);case"paramsChange":default:return!Ab(e,t)}}(a,o,o.routeConfig.runGuardsAndResolvers);c?s.canActivateChecks.push(new dC(r)):(o.data=a.data,o._resolvedData=a._resolvedData),e(t,i,o.component?l?l.children:null:n,r,s),c&&l&&l.outlet&&l.outlet.isActivated&&s.canDeactivateChecks.push(new fC(l.outlet.component,a))}else a&&mC(i,l,s),s.canActivateChecks.push(new dC(r)),e(t,null,o.component?l?l.children:null:n,r,s)}(t,o[t.value.outlet],n,r.concat([t.value]),s),delete o[t.value.outlet]}),Wy(o,(e,t)=>mC(e,n.getContext(t),s)),s}(n,t?t._root:null,i,[n.value])}function _C(e,t,i){const n=function(e){if(!e)return null;for(let t=e.parent;t;t=t.parent){const e=t.routeConfig;if(e&&e._loadedConfig)return e._loadedConfig}return null}(t);return(n?n.module.injector:i).get(e)}function mC(e,t,i){const n=gb(e),r=e.value;Wy(n,(e,n)=>{mC(e,r.component?t?t.children.getContext(n):null:t,i)}),i.canDeactivateChecks.push(new fC(r.component&&t&&t.outlet&&t.outlet.isActivated?t.outlet.component:null,r))}class gC{}function vC(e){return new v(t=>t.error(e))}class yC{constructor(e,t,i,n,r,s){this.rootComponentType=e,this.config=t,this.urlTree=i,this.url=n,this.paramsInheritanceStrategy=r,this.relativeLinkResolution=s}recognize(){const e=tC(this.urlTree.root,[],[],this.config.filter(e=>void 0===e.redirectTo),this.relativeLinkResolution).segmentGroup,t=this.processSegmentGroup(this.config,e,Fy);if(null===t)return null;const i=new wb([],Object.freeze({}),Object.freeze(Object.assign({},this.urlTree.queryParams)),this.urlTree.fragment,{},Fy,this.rootComponentType,null,this.urlTree.root,-1,{}),n=new mb(i,t),r=new Sb(this.url,n);return this.inheritParamsAndData(r._root),r}inheritParamsAndData(e){const t=e.value,i=Cb(t,this.paramsInheritanceStrategy);t.params=Object.freeze(i.params),t.data=Object.freeze(i.data),e.children.forEach(e=>this.inheritParamsAndData(e))}processSegmentGroup(e,t,i){return 0===t.segments.length&&t.hasChildren()?this.processChildren(e,t):this.processSegment(e,t,t.segments,i)}processChildren(e,t){const i=[];for(const r of Object.keys(t.children)){const n=t.children[r],s=Qb(e,r),o=this.processSegmentGroup(s,n,r);if(null===o)return null;i.push(...o)}const n=function(e){const t=[];for(const i of e){if(!bC(i)){t.push(i);continue}const e=t.find(e=>i.value.routeConfig===e.value.routeConfig);void 0!==e?e.children.push(...i.children):t.push(i)}return t}(i);return n.sort((e,t)=>e.value.outlet===Fy?-1:t.value.outlet===Fy?1:e.value.outlet.localeCompare(t.value.outlet)),n}processSegment(e,t,i,n){for(const r of e){const e=this.processSegmentAgainstRoute(r,t,i,n);if(null!==e)return e}return rC(t,i,n)?[]:null}processSegmentAgainstRoute(e,t,i,n){if(e.redirectTo||!nC(e,t,i,n))return null;let r,s=[],o=[];if("**"===e.path){const n=i.length>0?zy(i).parameters:{};r=new wb(i,n,Object.freeze(Object.assign({},this.urlTree.queryParams)),this.urlTree.fragment,SC(e),Xb(e),e.component,e,CC(t),wC(t)+i.length,kC(e))}else{const n=eC(t,e,i);if(!n.matched)return null;s=n.consumedSegments,o=i.slice(n.lastChild),r=new wb(s,n.parameters,Object.freeze(Object.assign({},this.urlTree.queryParams)),this.urlTree.fragment,SC(e),Xb(e),e.component,e,CC(t),wC(t)+s.length,kC(e))}const a=function(e){return e.children?e.children:e.loadChildren?e._loadedConfig.routes:[]}(e),{segmentGroup:l,slicedSegments:c}=tC(t,s,o,a.filter(e=>void 0===e.redirectTo),this.relativeLinkResolution);if(0===c.length&&l.hasChildren()){const e=this.processChildren(a,l);return null===e?null:[new mb(r,e)]}if(0===a.length&&0===c.length)return[new mb(r,[])];const h=Xb(e)===n,u=this.processSegment(a,l,c,h?Fy:n);return null===u?null:[new mb(r,u)]}}function bC(e){const t=e.value.routeConfig;return t&&""===t.path&&void 0===t.redirectTo}function CC(e){let t=e;for(;t._sourceSegment;)t=t._sourceSegment;return t}function wC(e){let t=e,i=t._segmentIndexShift?t._segmentIndexShift:0;for(;t._sourceSegment;)t=t._sourceSegment,i+=t._segmentIndexShift?t._segmentIndexShift:0;return i-1}function SC(e){return e.data||{}}function kC(e){return e.resolve||{}}function xC(e){return Sp(t=>{const i=e(t);return i?j(i).pipe(M(()=>t)):mp(t)})}class EC extends class{shouldDetach(e){return!1}store(e,t){}shouldAttach(e){return!1}retrieve(e){return null}shouldReuseRoute(e,t){return e.routeConfig===t.routeConfig}}{}const AC=new Qi("ROUTES");class TC{constructor(e,t,i,n){this.loader=e,this.compiler=t,this.onLoadStartListener=i,this.onLoadEndListener=n}load(e,t){if(t._loader$)return t._loader$;this.onLoadStartListener&&this.onLoadStartListener(t);const i=this.loadModuleFactory(t.loadChildren).pipe(M(i=>{this.onLoadEndListener&&this.onLoadEndListener(t);const n=i.create(e);return new Vb(qy(n.injector.get(AC,void 0,Se.Self|Se.Optional)).map(Yb),n)}),ny(e=>{throw t._loader$=void 0,e}));return t._loader$=new Z(i,()=>new S).pipe($()),t._loader$}loadModuleFactory(e){return"string"==typeof e?j(this.loader.load(e)):$y(e()).pipe(N(e=>e instanceof Cl?mp(e):j(this.compiler.compileModuleAsync(e))))}}class OC{constructor(){this.outlet=null,this.route=null,this.resolver=null,this.children=new RC,this.attachRef=null}}class RC{constructor(){this.contexts=new Map}onChildOutletCreated(e,t){const i=this.getOrCreateContext(e);i.outlet=t,this.contexts.set(e,i)}onChildOutletDestroyed(e){const t=this.getContext(e);t&&(t.outlet=null)}onOutletDeactivated(){const e=this.contexts;return this.contexts=new Map,e}onOutletReAttached(e){this.contexts=e}getOrCreateContext(e){let t=this.getContext(e);return t||(t=new OC,this.contexts.set(e,t)),t}getContext(e){return this.contexts.get(e)||null}}class LC{shouldProcessUrl(e){return!0}extract(e){return e}merge(e,t){return e}}function DC(e){throw e}function PC(e,t,i){return t.parse("/")}function IC(e,t){return mp(null)}let MC=(()=>{class e{constructor(e,t,i,n,r,s,o,a){this.rootComponentType=e,this.urlSerializer=t,this.rootContexts=i,this.location=n,this.config=a,this.lastSuccessfulNavigation=null,this.currentNavigation=null,this.disposed=!1,this.lastLocationChangeInfo=null,this.navigationId=0,this.isNgZoneEnabled=!1,this.events=new S,this.errorHandler=DC,this.malformedUriErrorHandler=PC,this.navigated=!1,this.lastSuccessfulId=-1,this.hooks={beforePreactivation:IC,afterPreactivation:IC},this.urlHandlingStrategy=new LC,this.routeReuseStrategy=new EC,this.onSameUrlNavigation="ignore",this.paramsInheritanceStrategy="emptyOnly",this.urlUpdateStrategy="deferred",this.relativeLinkResolution="corrected",this.ngModule=r.get(bl),this.console=r.get(_c);const l=r.get(Tc);this.isNgZoneEnabled=l instanceof Tc&&Tc.isInAngularZone(),this.resetConfig(a),this.currentUrlTree=new Gy(new Zy([],{}),{},null),this.rawUrlTree=this.currentUrlTree,this.browserUrlTree=this.currentUrlTree,this.configLoader=new TC(s,o,e=>this.triggerEvent(new Oy(e)),e=>this.triggerEvent(new Ry(e))),this.routerState=yb(this.currentUrlTree,this.rootComponentType),this.transitions=new Qv({id:0,currentUrlTree:this.currentUrlTree,currentRawUrl:this.currentUrlTree,extractedUrl:this.urlHandlingStrategy.extract(this.currentUrlTree),urlAfterRedirects:this.urlHandlingStrategy.extract(this.currentUrlTree),rawUrl:this.currentUrlTree,extras:{},resolve:null,reject:null,promise:Promise.resolve(!0),source:"imperative",restoredState:null,currentSnapshot:this.routerState.snapshot,targetSnapshot:null,currentRouterState:this.routerState,targetRouterState:null,guards:{canActivateChecks:[],canDeactivateChecks:[]},guardsResult:null}),this.navigations=this.setupNavigations(this.transitions),this.processNavigations()}setupNavigations(e){const t=this.events;return e.pipe(bp(e=>0!==e.id),M(e=>Object.assign(Object.assign({},e),{extractedUrl:this.urlHandlingStrategy.extract(e.rawUrl)})),Sp(e=>{let i=!1,n=!1;return mp(e).pipe(Rp(e=>{this.currentNavigation={id:e.id,initialUrl:e.currentRawUrl,extractedUrl:e.extractedUrl,trigger:e.source,extras:e.extras,previousNavigation:this.lastSuccessfulNavigation?Object.assign(Object.assign({},this.lastSuccessfulNavigation),{previousNavigation:null}):null}}),Sp(e=>{const i=!this.navigated||e.extractedUrl.toString()!==this.browserUrlTree.toString();if(("reload"===this.onSameUrlNavigation||i)&&this.urlHandlingStrategy.shouldProcessUrl(e.rawUrl))return mp(e).pipe(Sp(e=>{const i=this.transitions.getValue();return t.next(new by(e.id,this.serializeUrl(e.extractedUrl),e.source,e.restoredState)),i!==this.transitions.getValue()?hp:Promise.resolve(e)}),(n=this.ngModule.injector,r=this.configLoader,s=this.urlSerializer,o=this.config,Sp(e=>function(e,t,i,n,r){return new hC(e,t,i,n,r).apply()}(n,r,s,e.extractedUrl,o).pipe(M(t=>Object.assign(Object.assign({},e),{urlAfterRedirects:t}))))),Rp(e=>{this.currentNavigation=Object.assign(Object.assign({},this.currentNavigation),{finalUrl:e.urlAfterRedirects})}),function(e,t,i,n,r){return N(s=>function(e,t,i,n,r="emptyOnly",s="legacy"){try{const o=new yC(e,t,i,n,r,s).recognize();return null===o?vC(new gC):mp(o)}catch(o){return vC(o)}}(e,t,s.urlAfterRedirects,i(s.urlAfterRedirects),n,r).pipe(M(e=>Object.assign(Object.assign({},s),{targetSnapshot:e}))))}(this.rootComponentType,this.config,e=>this.serializeUrl(e),this.paramsInheritanceStrategy,this.relativeLinkResolution),Rp(e=>{"eager"===this.urlUpdateStrategy&&(e.extras.skipLocationChange||this.setBrowserUrl(e.urlAfterRedirects,!!e.extras.replaceUrl,e.id,e.extras.state),this.browserUrlTree=e.urlAfterRedirects);const i=new ky(e.id,this.serializeUrl(e.extractedUrl),this.serializeUrl(e.urlAfterRedirects),e.targetSnapshot);t.next(i)}));var n,r,s,o;if(i&&this.rawUrlTree&&this.urlHandlingStrategy.shouldProcessUrl(this.rawUrlTree)){const{id:i,extractedUrl:n,source:r,restoredState:s,extras:o}=e,a=new by(i,this.serializeUrl(n),r,s);t.next(a);const l=yb(n,this.rootComponentType).snapshot;return mp(Object.assign(Object.assign({},e),{targetSnapshot:l,urlAfterRedirects:n,extras:Object.assign(Object.assign({},o),{skipLocationChange:!1,replaceUrl:!1})}))}return this.rawUrlTree=e.rawUrl,this.browserUrlTree=e.urlAfterRedirects,e.resolve(null),hp}),xC(e=>{const{targetSnapshot:t,id:i,extractedUrl:n,rawUrl:r,extras:{skipLocationChange:s,replaceUrl:o}}=e;return this.hooks.beforePreactivation(t,{navigationId:i,appliedUrlTree:n,rawUrlTree:r,skipLocationChange:!!s,replaceUrl:!!o})}),Rp(e=>{const t=new xy(e.id,this.serializeUrl(e.extractedUrl),this.serializeUrl(e.urlAfterRedirects),e.targetSnapshot);this.triggerEvent(t)}),M(e=>Object.assign(Object.assign({},e),{guards:pC(e.targetSnapshot,e.currentSnapshot,this.rootContexts)})),function(e,t){return N(i=>{const{targetSnapshot:n,currentSnapshot:r,guards:{canActivateChecks:s,canDeactivateChecks:o}}=i;return 0===o.length&&0===s.length?mp(Object.assign(Object.assign({},i),{guardsResult:!0})):function(e,t,i,n){return j(e).pipe(N(e=>function(e,t,i,n,r){const s=t&&t.routeConfig?t.routeConfig.canDeactivate:null;return s&&0!==s.length?mp(s.map(s=>{const o=_C(s,t,r);let a;if(function(e){return e&&Ub(e.canDeactivate)}(o))a=$y(o.canDeactivate(e,t,i,n));else{if(!Ub(o))throw new Error("Invalid CanDeactivate guard");a=$y(o(e,t,i,n))}return a.pipe(my())})).pipe(Wb()):mp(!0)}(e.component,e.route,i,t,n)),my(e=>!0!==e,!0))}(o,n,r,e).pipe(N(i=>i&&"boolean"==typeof i?function(e,t,i,n){return j(t).pipe(Jp(t=>gp(function(e,t){return null!==e&&t&&t(new Ly(e)),mp(!0)}(t.route.parent,n),function(e,t){return null!==e&&t&&t(new Py(e)),mp(!0)}(t.route,n),function(e,t,i){const n=t[t.length-1],r=t.slice(0,t.length-1).reverse().map(e=>function(e){const t=e.routeConfig?e.routeConfig.canActivateChild:null;return t&&0!==t.length?{node:e,guards:t}:null}(e)).filter(e=>null!==e).map(t=>dp(()=>mp(t.guards.map(r=>{const s=_C(r,t.node,i);let o;if(function(e){return e&&Ub(e.canActivateChild)}(s))o=$y(s.canActivateChild(n,e));else{if(!Ub(s))throw new Error("Invalid CanActivateChild guard");o=$y(s(n,e))}return o.pipe(my())})).pipe(Wb())));return mp(r).pipe(Wb())}(e,t.path,i),function(e,t,i){const n=t.routeConfig?t.routeConfig.canActivate:null;return n&&0!==n.length?mp(n.map(n=>dp(()=>{const r=_C(n,t,i);let s;if(function(e){return e&&Ub(e.canActivate)}(r))s=$y(r.canActivate(t,e));else{if(!Ub(r))throw new Error("Invalid CanActivate guard");s=$y(r(t,e))}return s.pipe(my())}))).pipe(Wb()):mp(!0)}(e,t.route,i))),my(e=>!0!==e,!0))}(n,s,e,t):mp(i)),M(e=>Object.assign(Object.assign({},i),{guardsResult:e})))})}(this.ngModule.injector,e=>this.triggerEvent(e)),Rp(e=>{if(qb(e.guardsResult)){const t=jy(`Redirecting to "${this.serializeUrl(e.guardsResult)}"`);throw t.url=e.guardsResult,t}const t=new Ey(e.id,this.serializeUrl(e.extractedUrl),this.serializeUrl(e.urlAfterRedirects),e.targetSnapshot,!!e.guardsResult);this.triggerEvent(t)}),bp(e=>{if(!e.guardsResult){this.resetUrlToCurrentUrlTree();const i=new wy(e.id,this.serializeUrl(e.extractedUrl),"");return t.next(i),e.resolve(!1),!1}return!0}),xC(e=>{if(e.guards.canActivateChecks.length)return mp(e).pipe(Rp(e=>{const t=new Ay(e.id,this.serializeUrl(e.extractedUrl),this.serializeUrl(e.urlAfterRedirects),e.targetSnapshot);this.triggerEvent(t)}),Sp(e=>{let i=!1;return mp(e).pipe((n=this.paramsInheritanceStrategy,r=this.ngModule.injector,N(e=>{const{targetSnapshot:t,guards:{canActivateChecks:i}}=e;if(!i.length)return mp(e);let s=0;return j(i).pipe(Jp(e=>function(e,t,i,n){return function(e,t,i,n){const r=Object.keys(e);if(0===r.length)return mp({});const s={};return j(r).pipe(N(r=>function(e,t,i,n){const r=_C(e,t,n);return $y(r.resolve?r.resolve(t,i):r(t,i))}(e[r],t,i,n).pipe(Rp(e=>{s[r]=e}))),oy(1),N(()=>Object.keys(s).length===r.length?mp(s):hp))}(e._resolve,e,t,n).pipe(M(t=>(e._resolvedData=t,e.data=Object.assign(Object.assign({},e.data),Cb(e,i).resolve),null)))}(e.route,t,n,r)),Rp(()=>s++),oy(1),N(t=>s===i.length?mp(e):hp))})),Rp({next:()=>i=!0,complete:()=>{if(!i){const i=new wy(e.id,this.serializeUrl(e.extractedUrl),"At least one route resolver didn't emit any value.");t.next(i),e.resolve(!1)}}}));var n,r}),Rp(e=>{const t=new Ty(e.id,this.serializeUrl(e.extractedUrl),this.serializeUrl(e.urlAfterRedirects),e.targetSnapshot);this.triggerEvent(t)}))}),xC(e=>{const{targetSnapshot:t,id:i,extractedUrl:n,rawUrl:r,extras:{skipLocationChange:s,replaceUrl:o}}=e;return this.hooks.afterPreactivation(t,{navigationId:i,appliedUrlTree:n,rawUrlTree:r,skipLocationChange:!!s,replaceUrl:!!o})}),M(e=>{const t=function(e,t,i){const n=function e(t,i,n){if(n&&t.shouldReuseRoute(i.value,n.value.snapshot)){const r=n.value;r._futureSnapshot=i.value;const s=function(t,i,n){return i.children.map(i=>{for(const r of n.children)if(t.shouldReuseRoute(i.value,r.value.snapshot))return e(t,i,r);return e(t,i)})}(t,i,n);return new mb(r,s)}{const n=t.retrieve(i.value);if(n){const e=n.route;return function e(t,i){if(t.value.routeConfig!==i.value.routeConfig)throw new Error("Cannot reattach ActivatedRouteSnapshot created from a different route");if(t.children.length!==i.children.length)throw new Error("Cannot reattach ActivatedRouteSnapshot with a different number of children");i.value._futureSnapshot=t.value;for(let n=0;ne(t,i));return new mb(n,s)}}var r}(e,t._root,i?i._root:void 0);return new vb(n,t)}(this.routeReuseStrategy,e.targetSnapshot,e.currentRouterState);return Object.assign(Object.assign({},e),{targetRouterState:t})}),Rp(e=>{this.currentUrlTree=e.urlAfterRedirects,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,e.rawUrl),this.routerState=e.targetRouterState,"deferred"===this.urlUpdateStrategy&&(e.extras.skipLocationChange||this.setBrowserUrl(this.rawUrlTree,!!e.extras.replaceUrl,e.id,e.extras.state),this.browserUrlTree=e.urlAfterRedirects)}),(s=this.rootContexts,o=this.routeReuseStrategy,a=e=>this.triggerEvent(e),M(e=>(new jb(o,e.targetRouterState,e.currentRouterState,a).activate(s),e))),Rp({next(){i=!0},complete(){i=!0}}),(r=()=>{if(!i&&!n){this.resetUrlToCurrentUrlTree();const i=new wy(e.id,this.serializeUrl(e.extractedUrl),`Navigation ID ${e.id} is not equal to the current navigation id ${this.navigationId}`);t.next(i),e.resolve(!1)}this.currentNavigation=null},e=>e.lift(new gy(r))),ny(i=>{if(n=!0,(r=i)&&r.ngNavigationCancelingError){const n=qb(i.url);n||(this.navigated=!0,this.resetStateAndUrl(e.currentRouterState,e.currentUrlTree,e.rawUrl));const r=new wy(e.id,this.serializeUrl(e.extractedUrl),i.message);t.next(r),n?setTimeout(()=>{const t=this.urlHandlingStrategy.merge(i.url,this.rawUrlTree);this.scheduleNavigation(t,"imperative",null,{skipLocationChange:e.extras.skipLocationChange,replaceUrl:"eager"===this.urlUpdateStrategy},{resolve:e.resolve,reject:e.reject,promise:e.promise})},0):e.resolve(!1)}else{this.resetStateAndUrl(e.currentRouterState,e.currentUrlTree,e.rawUrl);const n=new Sy(e.id,this.serializeUrl(e.extractedUrl),i);t.next(n);try{e.resolve(this.errorHandler(i))}catch(s){e.reject(s)}}var r;return hp}));var r,s,o,a}))}resetRootComponentType(e){this.rootComponentType=e,this.routerState.root.component=this.rootComponentType}getTransition(){const e=this.transitions.value;return e.urlAfterRedirects=this.browserUrlTree,e}setTransition(e){this.transitions.next(Object.assign(Object.assign({},this.getTransition()),e))}initialNavigation(){this.setUpLocationChangeListener(),0===this.navigationId&&this.navigateByUrl(this.location.path(!0),{replaceUrl:!0})}setUpLocationChangeListener(){this.locationSubscription||(this.locationSubscription=this.location.subscribe(e=>{const t=this.extractLocationChangeInfoFromEvent(e);this.shouldScheduleNavigation(this.lastLocationChangeInfo,t)&&setTimeout(()=>{const{source:e,state:i,urlTree:n}=t,r={replaceUrl:!0};if(i){const e=Object.assign({},i);delete e.navigationId,0!==Object.keys(e).length&&(r.state=e)}this.scheduleNavigation(n,e,i,r)},0),this.lastLocationChangeInfo=t}))}extractLocationChangeInfoFromEvent(e){var t;return{source:"popstate"===e.type?"popstate":"hashchange",urlTree:this.parseUrl(e.url),state:(null===(t=e.state)||void 0===t?void 0:t.navigationId)?e.state:null,transitionId:this.getTransition().id}}shouldScheduleNavigation(e,t){if(!e)return!0;const i=t.urlTree.toString()===e.urlTree.toString();return!(t.transitionId===e.transitionId&&i&&("hashchange"===t.source&&"popstate"===e.source||"popstate"===t.source&&"hashchange"===e.source))}get url(){return this.serializeUrl(this.currentUrlTree)}getCurrentNavigation(){return this.currentNavigation}triggerEvent(e){this.events.next(e)}resetConfig(e){Kb(e),this.config=e.map(Yb),this.navigated=!1,this.lastSuccessfulId=-1}ngOnDestroy(){this.dispose()}dispose(){this.transitions.complete(),this.locationSubscription&&(this.locationSubscription.unsubscribe(),this.locationSubscription=void 0),this.disposed=!0}createUrlTree(e,t={}){const{relativeTo:i,queryParams:n,fragment:r,queryParamsHandling:s,preserveFragment:o}=t,a=i||this.routerState.root,l=o?this.currentUrlTree.fragment:r;let c=null;switch(s){case"merge":c=Object.assign(Object.assign({},this.currentUrlTree.queryParams),n);break;case"preserve":c=this.currentUrlTree.queryParams;break;default:c=n||null}return null!==c&&(c=this.removeEmptyProps(c)),function(e,t,i,n,r){if(0===i.length)return Rb(t.root,t.root,t,n,r);const s=function(e){if("string"==typeof e[0]&&1===e.length&&"/"===e[0])return new Lb(!0,0,e);let t=0,i=!1;const n=e.reduce((e,n,r)=>{if("object"==typeof n&&null!=n){if(n.outlets){const t={};return Wy(n.outlets,(e,i)=>{t[i]="string"==typeof e?e.split("/"):e}),[...e,{outlets:t}]}if(n.segmentPath)return[...e,n.segmentPath]}return"string"!=typeof n?[...e,n]:0===r?(n.split("/").forEach((n,r)=>{0==r&&"."===n||(0==r&&""===n?i=!0:".."===n?t++:""!=n&&e.push(n))}),e):[...e,n]},[]);return new Lb(i,t,n)}(i);if(s.toRoot())return Rb(t.root,new Zy([],{}),t,n,r);const o=function(e,t,i){if(e.isAbsolute)return new Db(t.root,!0,0);if(-1===i.snapshot._lastPathIndex){const e=i.snapshot._urlSegment;return new Db(e,e===t.root,0)}const n=Tb(e.commands[0])?0:1;return function(e,t,i){let n=e,r=t,s=i;for(;s>r;){if(s-=r,n=n.parent,!n)throw new Error("Invalid number of '../'");r=n.segments.length}return new Db(n,!1,r-s)}(i.snapshot._urlSegment,i.snapshot._lastPathIndex+n,e.numberOfDoubleDots)}(s,t,e),a=o.processChildren?Ib(o.segmentGroup,o.index,s.commands):Pb(o.segmentGroup,o.index,s.commands);return Rb(o.segmentGroup,a,t,n,r)}(a,this.currentUrlTree,e,c,l)}navigateByUrl(e,t={skipLocationChange:!1}){const i=qb(e)?e:this.parseUrl(e),n=this.urlHandlingStrategy.merge(i,this.rawUrlTree);return this.scheduleNavigation(n,"imperative",null,t)}navigate(e,t={skipLocationChange:!1}){return function(e){for(let t=0;t{const n=e[i];return null!=n&&(t[i]=n),t},{})}processNavigations(){this.navigations.subscribe(e=>{this.navigated=!0,this.lastSuccessfulId=e.id,this.events.next(new Cy(e.id,this.serializeUrl(e.extractedUrl),this.serializeUrl(this.currentUrlTree))),this.lastSuccessfulNavigation=this.currentNavigation,this.currentNavigation=null,e.resolve(!0)},e=>{this.console.warn("Unhandled Navigation Error: ")})}scheduleNavigation(e,t,i,n,r){if(this.disposed)return Promise.resolve(!1);const s=this.getTransition(),o="imperative"!==t&&"imperative"===(null==s?void 0:s.source),a=(this.lastSuccessfulId===s.id||this.currentNavigation?s.rawUrl:s.urlAfterRedirects).toString()===e.toString();if(o&&a)return Promise.resolve(!0);let l,c,h;r?(l=r.resolve,c=r.reject,h=r.promise):h=new Promise((e,t)=>{l=e,c=t});const u=++this.navigationId;return this.setTransition({id:u,source:t,restoredState:i,currentUrlTree:this.currentUrlTree,currentRawUrl:this.rawUrlTree,rawUrl:e,extras:n,resolve:l,reject:c,promise:h,currentSnapshot:this.routerState.snapshot,currentRouterState:this.routerState}),h.catch(e=>Promise.reject(e))}setBrowserUrl(e,t,i,n){const r=this.urlSerializer.serialize(e);n=n||{},this.location.isCurrentPathEqualTo(r)||t?this.location.replaceState(r,"",Object.assign(Object.assign({},n),{navigationId:i})):this.location.go(r,"",Object.assign(Object.assign({},n),{navigationId:i}))}resetStateAndUrl(e,t,i){this.routerState=e,this.currentUrlTree=t,this.rawUrlTree=this.urlHandlingStrategy.merge(this.currentUrlTree,i),this.resetUrlToCurrentUrlTree()}resetUrlToCurrentUrlTree(){this.location.replaceState(this.urlSerializer.serialize(this.rawUrlTree),"",{navigationId:this.lastSuccessfulId})}}return e.\u0275fac=function(t){return new(t||e)(mn(en),mn(Qy),mn(RC),mn(wh),mn(oo),mn(Xc),mn(xc),mn(void 0))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),FC=(()=>{class e{constructor(e,t,i,n,r){this.router=e,this.route=t,this.commands=[],this.onChanges=new S,null==i&&n.setAttribute(r.nativeElement,"tabindex","0")}ngOnChanges(e){this.onChanges.next(this)}set routerLink(e){this.commands=null!=e?Array.isArray(e)?e:[e]:[]}onClick(){const e={skipLocationChange:BC(this.skipLocationChange),replaceUrl:BC(this.replaceUrl),state:this.state};return this.router.navigateByUrl(this.urlTree,e),!0}get urlTree(){return this.router.createUrlTree(this.commands,{relativeTo:void 0!==this.relativeTo?this.relativeTo:this.route,queryParams:this.queryParams,fragment:this.fragment,queryParamsHandling:this.queryParamsHandling,preserveFragment:BC(this.preserveFragment)})}}return e.\u0275fac=function(t){return new(t||e)(xo(MC),xo(bb),Zi("tabindex"),xo(Ua),xo(ja))},e.\u0275dir=Qe({type:e,selectors:[["","routerLink","",5,"a",5,"area"]],hostBindings:function(e,t){1&e&&Ho("click",(function(){return t.onClick()}))},inputs:{routerLink:"routerLink",queryParams:"queryParams",fragment:"fragment",queryParamsHandling:"queryParamsHandling",preserveFragment:"preserveFragment",skipLocationChange:"skipLocationChange",replaceUrl:"replaceUrl",state:"state",relativeTo:"relativeTo"},features:[dt]}),e})(),HC=(()=>{class e{constructor(e,t,i){this.router=e,this.route=t,this.locationStrategy=i,this.commands=[],this.onChanges=new S,this.subscription=e.events.subscribe(e=>{e instanceof Cy&&this.updateTargetUrlAndHref()})}set routerLink(e){this.commands=null!=e?Array.isArray(e)?e:[e]:[]}ngOnChanges(e){this.updateTargetUrlAndHref(),this.onChanges.next(this)}ngOnDestroy(){this.subscription.unsubscribe()}onClick(e,t,i,n,r){if(0!==e||t||i||n||r)return!0;if("string"==typeof this.target&&"_self"!=this.target)return!0;const s={skipLocationChange:BC(this.skipLocationChange),replaceUrl:BC(this.replaceUrl),state:this.state};return this.router.navigateByUrl(this.urlTree,s),!1}updateTargetUrlAndHref(){this.href=this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.urlTree))}get urlTree(){return this.router.createUrlTree(this.commands,{relativeTo:void 0!==this.relativeTo?this.relativeTo:this.route,queryParams:this.queryParams,fragment:this.fragment,queryParamsHandling:this.queryParamsHandling,preserveFragment:BC(this.preserveFragment)})}}return e.\u0275fac=function(t){return new(t||e)(xo(MC),xo(bb),xo(gh))},e.\u0275dir=Qe({type:e,selectors:[["a","routerLink",""],["area","routerLink",""]],hostVars:2,hostBindings:function(e,t){1&e&&Ho("click",(function(e){return t.onClick(e.button,e.ctrlKey,e.shiftKey,e.altKey,e.metaKey)})),2&e&&(_a("href",t.href,er),Co("target",t.target))},inputs:{routerLink:"routerLink",target:"target",queryParams:"queryParams",fragment:"fragment",queryParamsHandling:"queryParamsHandling",preserveFragment:"preserveFragment",skipLocationChange:"skipLocationChange",replaceUrl:"replaceUrl",state:"state",relativeTo:"relativeTo"},features:[dt]}),e})();function BC(e){return""===e||!!e}let jC=(()=>{class e{constructor(e,t,i,n,r,s){this.router=e,this.element=t,this.renderer=i,this.cdr=n,this.link=r,this.linkWithHref=s,this.classes=[],this.isActive=!1,this.routerLinkActiveOptions={exact:!1},this.routerEventsSubscription=e.events.subscribe(e=>{e instanceof Cy&&this.update()})}ngAfterContentInit(){mp(this.links.changes,this.linksWithHrefs.changes,mp(null)).pipe(q()).subscribe(e=>{this.update(),this.subscribeToEachLinkOnChanges()})}subscribeToEachLinkOnChanges(){var e;null===(e=this.linkInputChangesSubscription)||void 0===e||e.unsubscribe();const t=[...this.links.toArray(),...this.linksWithHrefs.toArray(),this.link,this.linkWithHref].filter(e=>!!e).map(e=>e.onChanges);this.linkInputChangesSubscription=j(t).pipe(q()).subscribe(e=>{this.isActive!==this.isLinkActive(this.router)(e)&&this.update()})}set routerLinkActive(e){const t=Array.isArray(e)?e:e.split(" ");this.classes=t.filter(e=>!!e)}ngOnChanges(e){this.update()}ngOnDestroy(){var e;this.routerEventsSubscription.unsubscribe(),null===(e=this.linkInputChangesSubscription)||void 0===e||e.unsubscribe()}update(){this.links&&this.linksWithHrefs&&this.router.navigated&&Promise.resolve().then(()=>{const e=this.hasActiveLinks();this.isActive!==e&&(this.isActive=e,this.cdr.markForCheck(),this.classes.forEach(t=>{e?this.renderer.addClass(this.element.nativeElement,t):this.renderer.removeClass(this.element.nativeElement,t)}))})}isLinkActive(e){return t=>e.isActive(t.urlTree,this.routerLinkActiveOptions.exact)}hasActiveLinks(){const e=this.isLinkActive(this.router);return this.link&&e(this.link)||this.linkWithHref&&e(this.linkWithHref)||this.links.some(e)||this.linksWithHrefs.some(e)}}return e.\u0275fac=function(t){return new(t||e)(xo(MC),xo(ja),xo(Ua),xo(hl),xo(FC,8),xo(HC,8))},e.\u0275dir=Qe({type:e,selectors:[["","routerLinkActive",""]],contentQueries:function(e,t,i){if(1&e&&(tc(i,FC,1),tc(i,HC,1)),2&e){let e;Jl(e=ic())&&(t.links=e),Jl(e=ic())&&(t.linksWithHrefs=e)}},inputs:{routerLinkActiveOptions:"routerLinkActiveOptions",routerLinkActive:"routerLinkActive"},exportAs:["routerLinkActive"],features:[dt]}),e})(),NC=(()=>{class e{constructor(e,t,i,n,r){this.parentContexts=e,this.location=t,this.resolver=i,this.changeDetector=r,this.activated=null,this._activatedRoute=null,this.activateEvents=new Ul,this.deactivateEvents=new Ul,this.name=n||Fy,e.onChildOutletCreated(this.name,this)}ngOnDestroy(){this.parentContexts.onChildOutletDestroyed(this.name)}ngOnInit(){if(!this.activated){const e=this.parentContexts.getContext(this.name);e&&e.route&&(e.attachRef?this.attach(e.attachRef,e.route):this.activateWith(e.route,e.resolver||null))}}get isActivated(){return!!this.activated}get component(){if(!this.activated)throw new Error("Outlet is not activated");return this.activated.instance}get activatedRoute(){if(!this.activated)throw new Error("Outlet is not activated");return this._activatedRoute}get activatedRouteData(){return this._activatedRoute?this._activatedRoute.snapshot.data:{}}detach(){if(!this.activated)throw new Error("Outlet is not activated");this.location.detach();const e=this.activated;return this.activated=null,this._activatedRoute=null,e}attach(e,t){this.activated=e,this._activatedRoute=t,this.location.insert(e.hostView)}deactivate(){if(this.activated){const e=this.component;this.activated.destroy(),this.activated=null,this._activatedRoute=null,this.deactivateEvents.emit(e)}}activateWith(e,t){if(this.isActivated)throw new Error("Cannot activate an already activated outlet");this._activatedRoute=e;const i=(t=t||this.resolver).resolveComponentFactory(e._futureSnapshot.routeConfig.component),n=this.parentContexts.getOrCreateContext(this.name).children,r=new VC(e,n,this.location.injector);this.activated=this.location.createComponent(i,this.location.length,r),this.changeDetector.markForCheck(),this.activateEvents.emit(this.activated.instance)}}return e.\u0275fac=function(t){return new(t||e)(xo(RC),xo(Sl),xo(Ma),Zi("name"),xo(hl))},e.\u0275dir=Qe({type:e,selectors:[["router-outlet"]],outputs:{activateEvents:"activate",deactivateEvents:"deactivate"},exportAs:["outlet"]}),e})();class VC{constructor(e,t,i){this.route=e,this.childContexts=t,this.parent=i}get(e,t){return e===bb?this.route:e===RC?this.childContexts:this.parent.get(e,t)}}class UC{}class qC{preload(e,t){return mp(null)}}let zC=(()=>{class e{constructor(e,t,i,n,r){this.router=e,this.injector=n,this.preloadingStrategy=r,this.loader=new TC(t,i,t=>e.triggerEvent(new Oy(t)),t=>e.triggerEvent(new Ry(t)))}setUpPreloading(){this.subscription=this.router.events.pipe(bp(e=>e instanceof Cy),Jp(()=>this.preload())).subscribe(()=>{})}preload(){const e=this.injector.get(bl);return this.processRoutes(e,this.router.config)}ngOnDestroy(){this.subscription&&this.subscription.unsubscribe()}processRoutes(e,t){const i=[];for(const n of t)if(n.loadChildren&&!n.canLoad&&n._loadedConfig){const e=n._loadedConfig;i.push(this.processRoutes(e.module,e.routes))}else n.loadChildren&&!n.canLoad?i.push(this.preloadConfig(e,n)):n.children&&i.push(this.processRoutes(e,n.children));return j(i).pipe(q(),M(e=>{}))}preloadConfig(e,t){return this.preloadingStrategy.preload(t,()=>(t._loadedConfig?mp(t._loadedConfig):this.loader.load(e.injector,t)).pipe(N(e=>(t._loadedConfig=e,this.processRoutes(e.module,e.routes)))))}}return e.\u0275fac=function(t){return new(t||e)(mn(MC),mn(Xc),mn(xc),mn(oo),mn(UC))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})(),WC=(()=>{class e{constructor(e,t,i={}){this.router=e,this.viewportScroller=t,this.options=i,this.lastId=0,this.lastSource="imperative",this.restoredId=0,this.store={},i.scrollPositionRestoration=i.scrollPositionRestoration||"disabled",i.anchorScrolling=i.anchorScrolling||"disabled"}init(){"disabled"!==this.options.scrollPositionRestoration&&this.viewportScroller.setHistoryScrollRestoration("manual"),this.routerEventsSubscription=this.createScrollEvents(),this.scrollEventsSubscription=this.consumeScrollEvents()}createScrollEvents(){return this.router.events.subscribe(e=>{e instanceof by?(this.store[this.lastId]=this.viewportScroller.getScrollPosition(),this.lastSource=e.navigationTrigger,this.restoredId=e.restoredState?e.restoredState.navigationId:0):e instanceof Cy&&(this.lastId=e.id,this.scheduleScrollEvent(e,this.router.parseUrl(e.urlAfterRedirects).fragment))})}consumeScrollEvents(){return this.router.events.subscribe(e=>{e instanceof My&&(e.position?"top"===this.options.scrollPositionRestoration?this.viewportScroller.scrollToPosition([0,0]):"enabled"===this.options.scrollPositionRestoration&&this.viewportScroller.scrollToPosition(e.position):e.anchor&&"enabled"===this.options.anchorScrolling?this.viewportScroller.scrollToAnchor(e.anchor):"disabled"!==this.options.scrollPositionRestoration&&this.viewportScroller.scrollToPosition([0,0]))})}scheduleScrollEvent(e,t){this.router.triggerEvent(new My(e,"popstate"===this.lastSource?this.store[this.restoredId]:null,t))}ngOnDestroy(){this.routerEventsSubscription&&this.routerEventsSubscription.unsubscribe(),this.scrollEventsSubscription&&this.scrollEventsSubscription.unsubscribe()}}return e.\u0275fac=function(t){return new(t||e)(mn(MC),mn(Kh),mn(void 0))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();const $C=new Qi("ROUTER_CONFIGURATION"),KC=new Qi("ROUTER_FORROOT_GUARD"),GC=[wh,{provide:Qy,useClass:Jy},{provide:MC,useFactory:function(e,t,i,n,r,s,o,a={},l,c){const h=new MC(null,e,t,i,n,r,s,qy(o));if(l&&(h.urlHandlingStrategy=l),c&&(h.routeReuseStrategy=c),function(e,t){e.errorHandler&&(t.errorHandler=e.errorHandler),e.malformedUriErrorHandler&&(t.malformedUriErrorHandler=e.malformedUriErrorHandler),e.onSameUrlNavigation&&(t.onSameUrlNavigation=e.onSameUrlNavigation),e.paramsInheritanceStrategy&&(t.paramsInheritanceStrategy=e.paramsInheritanceStrategy),e.relativeLinkResolution&&(t.relativeLinkResolution=e.relativeLinkResolution),e.urlUpdateStrategy&&(t.urlUpdateStrategy=e.urlUpdateStrategy)}(a,h),a.enableTracing){const e=oh();h.events.subscribe(t=>{e.logGroup("Router Event: "+t.constructor.name),e.log(t.toString()),e.log(t),e.logGroupEnd()})}return h},deps:[Qy,RC,wh,oo,Xc,xc,AC,$C,[class{},new Cn],[class{},new Cn]]},RC,{provide:bb,useFactory:function(e){return e.routerState.root},deps:[MC]},{provide:Xc,useClass:eh},zC,qC,class{preload(e,t){return t().pipe(ny(()=>mp(null)))}},{provide:$C,useValue:{enableTracing:!1}}];function ZC(){return new zc("Router",MC)}let YC=(()=>{class e{constructor(e,t){}static forRoot(t,i){return{ngModule:e,providers:[GC,ew(t),{provide:KC,useFactory:JC,deps:[[MC,new Cn,new wn]]},{provide:$C,useValue:i||{}},{provide:gh,useFactory:QC,deps:[lh,[new bn(yh),new Cn],$C]},{provide:WC,useFactory:XC,deps:[MC,Kh,$C]},{provide:UC,useExisting:i&&i.preloadingStrategy?i.preloadingStrategy:qC},{provide:zc,multi:!0,useFactory:ZC},[tw,{provide:ac,multi:!0,useFactory:iw,deps:[tw]},{provide:rw,useFactory:nw,deps:[tw]},{provide:pc,multi:!0,useExisting:rw}]]}}static forChild(t){return{ngModule:e,providers:[ew(t)]}}}return e.\u0275fac=function(t){return new(t||e)(mn(KC,8),mn(MC,8))},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})();function XC(e,t,i){return i.scrollOffset&&t.setOffset(i.scrollOffset),new WC(e,t,i)}function QC(e,t,i={}){return i.useHash?new Ch(e,t):new bh(e,t)}function JC(e){return"guarded"}function ew(e){return[{provide:Ji,multi:!0,useValue:e},{provide:AC,multi:!0,useValue:e}]}let tw=(()=>{class e{constructor(e){this.injector=e,this.initNavigation=!1,this.resultOfPreactivationDone=new S}appInitializer(){return this.injector.get(hh,Promise.resolve(null)).then(()=>{let e=null;const t=new Promise(t=>e=t),i=this.injector.get(MC),n=this.injector.get($C);return"disabled"===n.initialNavigation?(i.setUpLocationChangeListener(),e(!0)):"enabled"===n.initialNavigation||"enabledBlocking"===n.initialNavigation?(i.hooks.afterPreactivation=()=>this.initNavigation?mp(null):(this.initNavigation=!0,e(!0),this.resultOfPreactivationDone),i.initialNavigation()):e(!0),t})}bootstrapListener(e){const t=this.injector.get($C),i=this.injector.get(zC),n=this.injector.get(WC),r=this.injector.get(MC),s=this.injector.get(Zc);e===s.components[0]&&("enabledNonBlocking"!==t.initialNavigation&&void 0!==t.initialNavigation||r.initialNavigation(),i.setUpPreloading(),n.init(),r.resetRootComponentType(s.componentTypes[0]),this.resultOfPreactivationDone.next(null),this.resultOfPreactivationDone.complete())}}return e.\u0275fac=function(t){return new(t||e)(mn(oo))},e.\u0275prov=pe({token:e,factory:e.\u0275fac}),e})();function iw(e){return e.appInitializer.bind(e)}function nw(e){return e.bootstrapListener.bind(e)}const rw=new Qi("Router Initializer");function sw(e,t,i,n){return new(i||(i=Promise))((function(r,s){function o(e){try{l(n.next(e))}catch(t){s(t)}}function a(e){try{l(n.throw(e))}catch(t){s(t)}}function l(e){var t;e.done?r(e.value):(t=e.value,t instanceof i?t:new i((function(e){e(t)}))).then(o,a)}l((n=n.apply(e,t||[])).next())}))}function ow(){return location.origin}function aw(e){let t="";if(e)for(const i in e)e.hasOwnProperty(i)&&null!=e[i]&&(t+=`${t?"&":""}${i}=${encodeURIComponent(e[i])}`);return t}const lw={subject:"Latest Commit",author:"system",commit:"HEAD",date:(new Date).toISOString()};let cw=(()=>{class e{constructor(e){this._http=e,this._sidebar=new Qv(!0),this._repo_list=new Qv([]),this._active_repo=new Qv(null),this._active_driver=new Qv(null),this._active_commit=new Qv(null),this._test_statuses=new Qv({}),this.sidebar=this._sidebar.asObservable(),this.repositories=this._repo_list.asObservable(),this.active_repo=this._active_repo.asObservable(),this.test_statuses=this._test_statuses.asObservable(),this.active_commit=this._active_commit.asObservable(),this.driver_list=this._active_repo.pipe(pg(300),Sp(e=>this.loadDrivers({repository:"Public"===e?void 0:e})),tm()),this.driver_versions=this._active_driver.pipe(Sp(e=>this.loadDriverVersions(e)),tm()),this.active_driver=this._active_driver.asObservable(),this.driver_commits=Ov([this._active_repo,this._active_driver]).pipe(Sp(e=>{const[t,i]=e;return this.loadDriverCommits(i,{repository:t})}),te()),this.loadRepositories(),this._test_statuses.next(JSON.parse(localStorage.getItem("HARNESS.statuses")||"{}")),this._test_statuses.subscribe(e=>localStorage.setItem("HARNESS.statuses",JSON.stringify(e)))}getRepository(){return this._active_repo.getValue()}getDriver(){return this._active_driver.getValue()}toggleSidebar(){this._sidebar.next(!this._sidebar.getValue())}getCommit(){return this._active_commit.getValue()}setTestStatus(e){const t=Object.assign({},this._test_statuses.getValue());t[`${this._active_repo.getValue()}|${this._active_driver.getValue()}`]=e,this._test_statuses.next(t)}setCommit(e){this._active_commit.next(e)}setRepository(e){e!==this._active_repo.getValue()&&this._active_repo.next(e)}setDriver(e){this._active_driver.next(e)}loadRepositories(){return sw(this,void 0,void 0,(function*(){const e=ow()+"/build/repositories",t=["Public",...(yield this._http.get(e).toPromise()).filter(e=>"."!==e[0])];this._repo_list.next(t),this._active_repo.getValue()||this._active_repo.next(t[0])}))}loadRepositoryCommits(e={}){return sw(this,void 0,void 0,(function*(){const e=ow()+"/build/repositories_commits",t=yield this._http.get(e).toPromise();return[lw,...t]}))}loadDrivers(e={}){return sw(this,void 0,void 0,(function*(){const t=aw(e),i=`${ow()}/build${t?"?"+t:""}`;return this._http.get(i).toPromise()}))}loadDriverCommits(e,t={}){return sw(this,void 0,void 0,(function*(){const t=`${ow()}/build/${encodeURIComponent(e)}/commits`,i=yield this._http.get(t).toPromise();return this._active_commit.next(lw),[lw,...i]}))}loadDriverVersions(e){return sw(this,void 0,void 0,(function*(){const t=`${ow()}/build/${encodeURIComponent(e)}`;return this._http.get(t).toPromise()}))}cleanDriverVersions(e,t){return sw(this,void 0,void 0,(function*(){const i=aw(t),n=`${ow()}/build/${encodeURIComponent(e)}${i?"?"+i:""}`;return this._http.delete(n).toPromise()}))}compileDriver(e){return sw(this,void 0,void 0,(function*(){const t=aw(e),i=ow()+"/build";return this._http.post(i,t).toPromise()}))}}return e.\u0275fac=function(t){return new(t||e)(mn(m_))},e.\u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"}),e})();var hw=i("6i1C");let uw=(()=>{class e{constructor(e,t){this._http=e,this._build=t,this._active_spec=new Qv(null),this._active_commit=new Qv(null),this._settings=new Qv({}),this.active_spec=this._active_spec.asObservable(),this.active_commit=this._active_commit.asObservable(),this.settings=this._settings.asObservable(),this.spec_list=this._build.active_repo.pipe(Sp(e=>this.loadSpecFiles({repository:"Public"===e?void 0:e})),tm()),this.commit_list=this._active_spec.pipe(bp(e=>!!e),Sp(e=>this.loadSpecCommits(e,{repository:"Public"===e?void 0:e})),tm()),Ov([this._build.active_driver,this.spec_list]).subscribe(e=>sw(this,void 0,void 0,(function*(){const[t,i]=e,n=i.map(e=>({spec:e,similarity:Object(hw.stringSimilarity)(e,t)}));n.sort((e,t)=>t.similarity-e.similarity),this._active_spec.next(n[0].spec)})))}setSpec(e){this._active_spec.next(e)}setCommit(e){this._active_commit.next(e)}setSettings(e){this._settings.next(Object.assign(Object.assign({},this._settings.getValue()),e))}loadSpecFiles(e={}){return sw(this,void 0,void 0,(function*(){const t=aw(e),i=`${ow()}/test${t?"?"+t:""}`;return this._http.get(i).toPromise()}))}loadSpecCommits(e,t){return sw(this,void 0,void 0,(function*(){const t=`${ow()}/test/${encodeURIComponent(e)}/commits`,i=yield this._http.get(t).toPromise();return this._active_commit.next(lw),[lw,...i]}))}runSpec(e){var t,i;return sw(this,void 0,void 0,(function*(){const n=this._build.getRepository()||e.repository,r=aw(e={repository:"Public"===n?void 0:n,driver:this._build.getDriver()||e.driver,spec:this._active_spec.getValue()||e.spec,commit:(null===(t=this._build.getCommit())||void 0===t?void 0:t.commit)||e.commit,spec_commit:(null===(i=this._active_commit.getValue())||void 0===i?void 0:i.commit)||e.spec_commit,force:this._settings.getValue().force||e.force,debug:this._settings.getValue().debug_symbols||e.debug}),s=`${ow()}/test${r?"?"+r:""}`;return this._http.post(s,e,{responseType:"text"}).toPromise()}))}}return e.\u0275fac=function(t){return new(t||e)(mn(m_),mn(cw))},e.\u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"}),e})();const dw=["underline"],fw=["connectionContainer"],pw=["inputContainer"],_w=["label"];function mw(e,t){1&e&&(Lo(0),To(1,"div",14),Ro(2,"div",15),Ro(3,"div",16),Ro(4,"div",17),Oo(),To(5,"div",18),Ro(6,"div",15),Ro(7,"div",16),Ro(8,"div",17),Oo(),Do())}function gw(e,t){1&e&&(To(0,"div",19),Wo(1,1),Oo())}function vw(e,t){if(1&e&&(Lo(0),Wo(1,2),To(2,"span"),da(3),Oo(),Do()),2&e){const e=Uo(2);Zr(3),fa(e._control.placeholder)}}function yw(e,t){1&e&&Wo(0,3,["*ngSwitchCase","true"])}function bw(e,t){1&e&&(To(0,"span",23),da(1," *"),Oo())}function Cw(e,t){if(1&e){const e=Po();To(0,"label",20,21),Ho("cdkObserveContent",(function(){return Bt(e),Uo().updateOutlineGap()})),So(2,vw,4,1,"ng-container",12),So(3,yw,1,0,"ng-content",12),So(4,bw,2,0,"span",22),Oo()}if(2&e){const e=Uo();Jo("mat-empty",e._control.empty&&!e._shouldAlwaysFloat())("mat-form-field-empty",e._control.empty&&!e._shouldAlwaysFloat())("mat-accent","accent"==e.color)("mat-warn","warn"==e.color),Eo("cdkObserveContentDisabled","outline"!=e.appearance)("id",e._labelId)("ngSwitch",e._hasLabel()),Co("for",e._control.id)("aria-owns",e._control.id),Zr(2),Eo("ngSwitchCase",!1),Zr(1),Eo("ngSwitchCase",!0),Zr(1),Eo("ngIf",!e.hideRequiredMarker&&e._control.required&&!e._control.disabled)}}function ww(e,t){1&e&&(To(0,"div",24),Wo(1,4),Oo())}function Sw(e,t){if(1&e&&(To(0,"div",25,26),Ro(2,"span",27),Oo()),2&e){const e=Uo();Zr(2),Jo("mat-accent","accent"==e.color)("mat-warn","warn"==e.color)}}function kw(e,t){1&e&&(To(0,"div"),Wo(1,5),Oo()),2&e&&Eo("@transitionMessages",Uo()._subscriptAnimationState)}function xw(e,t){if(1&e&&(To(0,"div",31),da(1),Oo()),2&e){const e=Uo(2);Eo("id",e._hintLabelId),Zr(1),fa(e.hintLabel)}}function Ew(e,t){if(1&e&&(To(0,"div",28),So(1,xw,2,2,"div",29),Wo(2,6),Ro(3,"div",30),Wo(4,7),Oo()),2&e){const e=Uo();Eo("@transitionMessages",e._subscriptAnimationState),Zr(1),Eo("ngIf",e.hintLabel)}}const Aw=["*",[["","matPrefix",""]],[["mat-placeholder"]],[["mat-label"]],[["","matSuffix",""]],[["mat-error"]],[["mat-hint",3,"align","end"]],[["mat-hint","align","end"]]],Tw=["*","[matPrefix]","mat-placeholder","mat-label","[matSuffix]","mat-error","mat-hint:not([align='end'])","mat-hint[align='end']"],Ow=new Qi("MatError"),Rw={transitionMessages:Eu("transitionMessages",[Ru("enter",Ou({opacity:1,transform:"translateY(0%)"})),Lu("void => enter",[Ou({opacity:0,transform:"translateY(-5px)"}),Au("300ms cubic-bezier(0.55, 0, 0.55, 0.2)")])])};let Lw=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275dir=Qe({type:e}),e})();const Dw=new Qi("MatHint");let Pw=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275dir=Qe({type:e,selectors:[["mat-label"]]}),e})(),Iw=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275dir=Qe({type:e,selectors:[["mat-placeholder"]]}),e})();const Mw=new Qi("MatPrefix");let Fw=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275dir=Qe({type:e,selectors:[["","matPrefix",""]],features:[Da([{provide:Mw,useExisting:e}])]}),e})();const Hw=new Qi("MatSuffix");let Bw=0;class jw{constructor(e){this._elementRef=e}}const Nw=Wg(jw,"primary"),Vw=new Qi("MAT_FORM_FIELD_DEFAULT_OPTIONS"),Uw=new Qi("MatFormField");let qw=(()=>{class e extends Nw{constructor(e,t,i,n,r,s,o,a){super(e),this._elementRef=e,this._changeDetectorRef=t,this._dir=n,this._defaults=r,this._platform=s,this._ngZone=o,this._outlineGapCalculationNeededImmediately=!1,this._outlineGapCalculationNeededOnStable=!1,this._destroyed=new S,this._showAlwaysAnimate=!1,this._subscriptAnimationState="",this._hintLabel="",this._hintLabelId="mat-hint-"+Bw++,this._labelId="mat-form-field-label-"+Bw++,this.floatLabel=this._getDefaultFloatLabelState(),this._animationsEnabled="NoopAnimations"!==a,this.appearance=r&&r.appearance?r.appearance:"legacy",this._hideRequiredMarker=!(!r||null==r.hideRequiredMarker)&&r.hideRequiredMarker}get appearance(){return this._appearance}set appearance(e){const t=this._appearance;this._appearance=e||this._defaults&&this._defaults.appearance||"legacy","outline"===this._appearance&&t!==e&&(this._outlineGapCalculationNeededOnStable=!0)}get hideRequiredMarker(){return this._hideRequiredMarker}set hideRequiredMarker(e){this._hideRequiredMarker=D_(e)}_shouldAlwaysFloat(){return"always"===this.floatLabel&&!this._showAlwaysAnimate}_canLabelFloat(){return"never"!==this.floatLabel}get hintLabel(){return this._hintLabel}set hintLabel(e){this._hintLabel=e,this._processHints()}get floatLabel(){return"legacy"!==this.appearance&&"never"===this._floatLabel?"auto":this._floatLabel}set floatLabel(e){e!==this._floatLabel&&(this._floatLabel=e||this._getDefaultFloatLabelState(),this._changeDetectorRef.markForCheck())}get _control(){return this._explicitFormFieldControl||this._controlNonStatic||this._controlStatic}set _control(e){this._explicitFormFieldControl=e}getLabelId(){return this._hasFloatingLabel()?this._labelId:null}getConnectedOverlayOrigin(){return this._connectionContainerRef||this._elementRef}ngAfterContentInit(){this._validateControlChild();const e=this._control;e.controlType&&this._elementRef.nativeElement.classList.add("mat-form-field-type-"+e.controlType),e.stateChanges.pipe(K_(null)).subscribe(()=>{this._validatePlaceholders(),this._syncDescribedByIds(),this._changeDetectorRef.markForCheck()}),e.ngControl&&e.ngControl.valueChanges&&e.ngControl.valueChanges.pipe(z_(this._destroyed)).subscribe(()=>this._changeDetectorRef.markForCheck()),this._ngZone.runOutsideAngular(()=>{this._ngZone.onStable.pipe(z_(this._destroyed)).subscribe(()=>{this._outlineGapCalculationNeededOnStable&&this.updateOutlineGap()})}),W(this._prefixChildren.changes,this._suffixChildren.changes).subscribe(()=>{this._outlineGapCalculationNeededOnStable=!0,this._changeDetectorRef.markForCheck()}),this._hintChildren.changes.pipe(K_(null)).subscribe(()=>{this._processHints(),this._changeDetectorRef.markForCheck()}),this._errorChildren.changes.pipe(K_(null)).subscribe(()=>{this._syncDescribedByIds(),this._changeDetectorRef.markForCheck()}),this._dir&&this._dir.change.pipe(z_(this._destroyed)).subscribe(()=>{"function"==typeof requestAnimationFrame?this._ngZone.runOutsideAngular(()=>{requestAnimationFrame(()=>this.updateOutlineGap())}):this.updateOutlineGap()})}ngAfterContentChecked(){this._validateControlChild(),this._outlineGapCalculationNeededImmediately&&this.updateOutlineGap()}ngAfterViewInit(){this._subscriptAnimationState="enter",this._changeDetectorRef.detectChanges()}ngOnDestroy(){this._destroyed.next(),this._destroyed.complete()}_shouldForward(e){const t=this._control?this._control.ngControl:null;return t&&t[e]}_hasPlaceholder(){return!!(this._control&&this._control.placeholder||this._placeholderChild)}_hasLabel(){return!(!this._labelChildNonStatic&&!this._labelChildStatic)}_shouldLabelFloat(){return this._canLabelFloat()&&(this._control&&this._control.shouldLabelFloat||this._shouldAlwaysFloat())}_hideControlPlaceholder(){return"legacy"===this.appearance&&!this._hasLabel()||this._hasLabel()&&!this._shouldLabelFloat()}_hasFloatingLabel(){return this._hasLabel()||"legacy"===this.appearance&&this._hasPlaceholder()}_getDisplayedMessages(){return this._errorChildren&&this._errorChildren.length>0&&this._control.errorState?"error":"hint"}_animateAndLockLabel(){this._hasFloatingLabel()&&this._canLabelFloat()&&(this._animationsEnabled&&this._label&&(this._showAlwaysAnimate=!0,_p(this._label.nativeElement,"transitionend").pipe(Ap(1)).subscribe(()=>{this._showAlwaysAnimate=!1})),this.floatLabel="always",this._changeDetectorRef.markForCheck())}_validatePlaceholders(){}_processHints(){this._validateHints(),this._syncDescribedByIds()}_validateHints(){}_getDefaultFloatLabelState(){return this._defaults&&this._defaults.floatLabel||"auto"}_syncDescribedByIds(){if(this._control){let e=[];if(this._control.userAriaDescribedBy&&"string"==typeof this._control.userAriaDescribedBy&&e.push(...this._control.userAriaDescribedBy.split(" ")),"hint"===this._getDisplayedMessages()){const t=this._hintChildren?this._hintChildren.find(e=>"start"===e.align):null,i=this._hintChildren?this._hintChildren.find(e=>"end"===e.align):null;t?e.push(t.id):this._hintLabel&&e.push(this._hintLabelId),i&&e.push(i.id)}else this._errorChildren&&e.push(...this._errorChildren.map(e=>e.id));this._control.setDescribedByIds(e)}}_validateControlChild(){}updateOutlineGap(){const e=this._label?this._label.nativeElement:null;if("outline"!==this.appearance||!e||!e.children.length||!e.textContent.trim())return;if(!this._platform.isBrowser)return;if(!this._isAttachedToDOM())return void(this._outlineGapCalculationNeededImmediately=!0);let t=0,i=0;const n=this._connectionContainerRef.nativeElement,r=n.querySelectorAll(".mat-form-field-outline-start"),s=n.querySelectorAll(".mat-form-field-outline-gap");if(this._label&&this._label.nativeElement.children.length){const r=n.getBoundingClientRect();if(0===r.width&&0===r.height)return this._outlineGapCalculationNeededOnStable=!0,void(this._outlineGapCalculationNeededImmediately=!1);const s=this._getStartEnd(r),o=e.children,a=this._getStartEnd(o[0].getBoundingClientRect());let l=0;for(let e=0;e0?.75*l+10:0}for(let o=0;o{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[Wh,qg,Cg],qg]}),e})();function Ww(e,t){return new v(i=>{const n=e.length;if(0===n)return void i.complete();const r=new Array(n);let s=0,o=0;for(let a=0;a{c||(c=!0,o++),r[a]=e},error:e=>i.error(e),complete:()=>{s++,s!==n&&c||(o===n&&i.next(t?t.reduce((e,t,i)=>(e[t]=r[i],e),{}):r),i.complete())}}))}})}const $w=new Qi("NgValueAccessor"),Kw={provide:$w,useExisting:ae(()=>Zw),multi:!0},Gw=new Qi("CompositionEventMode");let Zw=(()=>{class e{constructor(e,t,i){this._renderer=e,this._elementRef=t,this._compositionMode=i,this.onChange=e=>{},this.onTouched=()=>{},this._composing=!1,null==this._compositionMode&&(this._compositionMode=!function(){const e=oh()?oh().getUserAgent():"";return/android (\d+)/.test(e.toLowerCase())}())}writeValue(e){this._renderer.setProperty(this._elementRef.nativeElement,"value",null==e?"":e)}registerOnChange(e){this.onChange=e}registerOnTouched(e){this.onTouched=e}setDisabledState(e){this._renderer.setProperty(this._elementRef.nativeElement,"disabled",e)}_handleInput(e){(!this._compositionMode||this._compositionMode&&!this._composing)&&this.onChange(e)}_compositionStart(){this._composing=!0}_compositionEnd(e){this._composing=!1,this._compositionMode&&this.onChange(e)}}return e.\u0275fac=function(t){return new(t||e)(xo(Ua),xo(ja),xo(Gw,8))},e.\u0275dir=Qe({type:e,selectors:[["input","formControlName","",3,"type","checkbox"],["textarea","formControlName",""],["input","formControl","",3,"type","checkbox"],["textarea","formControl",""],["input","ngModel","",3,"type","checkbox"],["textarea","ngModel",""],["","ngDefaultControl",""]],hostBindings:function(e,t){1&e&&Ho("input",(function(e){return t._handleInput(e.target.value)}))("blur",(function(){return t.onTouched()}))("compositionstart",(function(){return t._compositionStart()}))("compositionend",(function(e){return t._compositionEnd(e.target.value)}))},features:[Da([Kw])]}),e})();const Yw=new Qi("NgValidators"),Xw=new Qi("NgAsyncValidators");function Qw(e){return null!=e}function Jw(e){const t=Io(e)?j(e):e;return Fo(t),t}function eS(e){let t={};return e.forEach(e=>{t=null!=e?Object.assign(Object.assign({},t),e):t}),0===Object.keys(t).length?null:t}function tS(e,t){return t.map(t=>t(e))}function iS(e){return e.map(e=>function(e){return!e.validate}(e)?e:t=>e.validate(t))}function nS(e){return null!=e?function(e){if(!e)return null;const t=e.filter(Qw);return 0==t.length?null:function(e){return eS(tS(e,t))}}(iS(e)):null}function rS(e){return null!=e?function(e){if(!e)return null;const t=e.filter(Qw);return 0==t.length?null:function(e){return function(...e){if(1===e.length){const t=e[0];if(l(t))return Ww(t,null);if(c(t)&&Object.getPrototypeOf(t)===Object.prototype){const e=Object.keys(t);return Ww(e.map(e=>t[e]),e)}}if("function"==typeof e[e.length-1]){const t=e.pop();return Ww(e=1===e.length&&l(e[0])?e[0]:e,null).pipe(M(e=>t(...e)))}return Ww(e,null)}(tS(e,t).map(Jw)).pipe(M(eS))}}(iS(e)):null}function sS(e,t){return null===e?[t]:Array.isArray(e)?[...e,t]:[e,t]}function oS(e){return e._rawValidators}function aS(e){return e._rawAsyncValidators}let lS=(()=>{class e{constructor(){this._rawValidators=[],this._rawAsyncValidators=[],this._onDestroyCallbacks=[]}get value(){return this.control?this.control.value:null}get valid(){return this.control?this.control.valid:null}get invalid(){return this.control?this.control.invalid:null}get pending(){return this.control?this.control.pending:null}get disabled(){return this.control?this.control.disabled:null}get enabled(){return this.control?this.control.enabled:null}get errors(){return this.control?this.control.errors:null}get pristine(){return this.control?this.control.pristine:null}get dirty(){return this.control?this.control.dirty:null}get touched(){return this.control?this.control.touched:null}get status(){return this.control?this.control.status:null}get untouched(){return this.control?this.control.untouched:null}get statusChanges(){return this.control?this.control.statusChanges:null}get valueChanges(){return this.control?this.control.valueChanges:null}get path(){return null}_setValidators(e){this._rawValidators=e||[],this._composedValidatorFn=nS(this._rawValidators)}_setAsyncValidators(e){this._rawAsyncValidators=e||[],this._composedAsyncValidatorFn=rS(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn||null}get asyncValidator(){return this._composedAsyncValidatorFn||null}_registerOnDestroy(e){this._onDestroyCallbacks.push(e)}_invokeOnDestroyCallbacks(){this._onDestroyCallbacks.forEach(e=>e()),this._onDestroyCallbacks=[]}reset(e){this.control&&this.control.reset(e)}hasError(e,t){return!!this.control&&this.control.hasError(e,t)}getError(e,t){return this.control?this.control.getError(e,t):null}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275dir=Qe({type:e}),e})(),cS=(()=>{class e extends lS{get formDirective(){return null}get path(){return null}}return e.\u0275fac=function(t){return hS(t||e)},e.\u0275dir=Qe({type:e,features:[lo]}),e})();const hS=Ki(cS);class uS extends lS{constructor(){super(...arguments),this._parent=null,this.name=null,this.valueAccessor=null}}let dS=(()=>{class e extends class{constructor(e){this._cd=e}is(e){var t,i;return!!(null===(i=null===(t=this._cd)||void 0===t?void 0:t.control)||void 0===i?void 0:i[e])}}{constructor(e){super(e)}}return e.\u0275fac=function(t){return new(t||e)(xo(uS,2))},e.\u0275dir=Qe({type:e,selectors:[["","formControlName",""],["","ngModel",""],["","formControl",""]],hostVars:14,hostBindings:function(e,t){2&e&&Jo("ng-untouched",t.is("untouched"))("ng-touched",t.is("touched"))("ng-pristine",t.is("pristine"))("ng-dirty",t.is("dirty"))("ng-valid",t.is("valid"))("ng-invalid",t.is("invalid"))("ng-pending",t.is("pending"))},features:[lo]}),e})();function fS(e,t){mS(e,t,!0),t.valueAccessor.writeValue(e.value),function(e,t){t.valueAccessor.registerOnChange(i=>{e._pendingValue=i,e._pendingChange=!0,e._pendingDirty=!0,"change"===e.updateOn&&vS(e,t)})}(e,t),function(e,t){const i=(e,i)=>{t.valueAccessor.writeValue(e),i&&t.viewToModelUpdate(e)};e.registerOnChange(i),t._registerOnDestroy(()=>{e._unregisterOnChange(i)})}(e,t),function(e,t){t.valueAccessor.registerOnTouched(()=>{e._pendingTouched=!0,"blur"===e.updateOn&&e._pendingChange&&vS(e,t),"submit"!==e.updateOn&&e.markAsTouched()})}(e,t),function(e,t){if(t.valueAccessor.setDisabledState){const i=e=>{t.valueAccessor.setDisabledState(e)};e.registerOnDisabledChange(i),t._registerOnDestroy(()=>{e._unregisterOnDisabledChange(i)})}}(e,t)}function pS(e,t,i=!0){const n=()=>{};t.valueAccessor&&(t.valueAccessor.registerOnChange(n),t.valueAccessor.registerOnTouched(n)),gS(e,t,!0),e&&(t._invokeOnDestroyCallbacks(),e._registerOnCollectionChange(()=>{}))}function _S(e,t){e.forEach(e=>{e.registerOnValidatorChange&&e.registerOnValidatorChange(t)})}function mS(e,t,i){const n=oS(e);null!==t.validator?e.setValidators(sS(n,t.validator)):"function"==typeof n&&e.setValidators([n]);const r=aS(e);if(null!==t.asyncValidator?e.setAsyncValidators(sS(r,t.asyncValidator)):"function"==typeof r&&e.setAsyncValidators([r]),i){const i=()=>e.updateValueAndValidity();_S(t._rawValidators,i),_S(t._rawAsyncValidators,i)}}function gS(e,t,i){let n=!1;if(null!==e){if(null!==t.validator){const i=oS(e);if(Array.isArray(i)&&i.length>0){const r=i.filter(e=>e!==t.validator);r.length!==i.length&&(n=!0,e.setValidators(r))}}if(null!==t.asyncValidator){const i=aS(e);if(Array.isArray(i)&&i.length>0){const r=i.filter(e=>e!==t.asyncValidator);r.length!==i.length&&(n=!0,e.setAsyncValidators(r))}}}if(i){const e=()=>{};_S(t._rawValidators,e),_S(t._rawAsyncValidators,e)}return n}function vS(e,t){e._pendingDirty&&e.markAsDirty(),e.setValue(e._pendingValue,{emitModelToViewChange:!1}),t.viewToModelUpdate(e._pendingValue),e._pendingChange=!1}function yS(e,t){mS(e,t,!1)}function bS(e,t){e._syncPendingControls(),t.forEach(e=>{const t=e.control;"submit"===t.updateOn&&t._pendingChange&&(e.viewToModelUpdate(t._pendingValue),t._pendingChange=!1)})}function CS(e,t){const i=e.indexOf(t);i>-1&&e.splice(i,1)}const wS="VALID",SS="INVALID",kS="PENDING",xS="DISABLED";function ES(e){return(RS(e)?e.validators:e)||null}function AS(e){return Array.isArray(e)?nS(e):e||null}function TS(e,t){return(RS(t)?t.asyncValidators:e)||null}function OS(e){return Array.isArray(e)?rS(e):e||null}function RS(e){return null!=e&&!Array.isArray(e)&&"object"==typeof e}class LS{constructor(e,t){this._hasOwnPendingAsyncValidator=!1,this._onCollectionChange=()=>{},this._parent=null,this.pristine=!0,this.touched=!1,this._onDisabledChange=[],this._rawValidators=e,this._rawAsyncValidators=t,this._composedValidatorFn=AS(this._rawValidators),this._composedAsyncValidatorFn=OS(this._rawAsyncValidators)}get validator(){return this._composedValidatorFn}set validator(e){this._rawValidators=this._composedValidatorFn=e}get asyncValidator(){return this._composedAsyncValidatorFn}set asyncValidator(e){this._rawAsyncValidators=this._composedAsyncValidatorFn=e}get parent(){return this._parent}get valid(){return this.status===wS}get invalid(){return this.status===SS}get pending(){return this.status==kS}get disabled(){return this.status===xS}get enabled(){return this.status!==xS}get dirty(){return!this.pristine}get untouched(){return!this.touched}get updateOn(){return this._updateOn?this._updateOn:this.parent?this.parent.updateOn:"change"}setValidators(e){this._rawValidators=e,this._composedValidatorFn=AS(e)}setAsyncValidators(e){this._rawAsyncValidators=e,this._composedAsyncValidatorFn=OS(e)}clearValidators(){this.validator=null}clearAsyncValidators(){this.asyncValidator=null}markAsTouched(e={}){this.touched=!0,this._parent&&!e.onlySelf&&this._parent.markAsTouched(e)}markAllAsTouched(){this.markAsTouched({onlySelf:!0}),this._forEachChild(e=>e.markAllAsTouched())}markAsUntouched(e={}){this.touched=!1,this._pendingTouched=!1,this._forEachChild(e=>{e.markAsUntouched({onlySelf:!0})}),this._parent&&!e.onlySelf&&this._parent._updateTouched(e)}markAsDirty(e={}){this.pristine=!1,this._parent&&!e.onlySelf&&this._parent.markAsDirty(e)}markAsPristine(e={}){this.pristine=!0,this._pendingDirty=!1,this._forEachChild(e=>{e.markAsPristine({onlySelf:!0})}),this._parent&&!e.onlySelf&&this._parent._updatePristine(e)}markAsPending(e={}){this.status=kS,!1!==e.emitEvent&&this.statusChanges.emit(this.status),this._parent&&!e.onlySelf&&this._parent.markAsPending(e)}disable(e={}){const t=this._parentMarkedDirty(e.onlySelf);this.status=xS,this.errors=null,this._forEachChild(t=>{t.disable(Object.assign(Object.assign({},e),{onlySelf:!0}))}),this._updateValue(),!1!==e.emitEvent&&(this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._updateAncestors(Object.assign(Object.assign({},e),{skipPristineCheck:t})),this._onDisabledChange.forEach(e=>e(!0))}enable(e={}){const t=this._parentMarkedDirty(e.onlySelf);this.status=wS,this._forEachChild(t=>{t.enable(Object.assign(Object.assign({},e),{onlySelf:!0}))}),this.updateValueAndValidity({onlySelf:!0,emitEvent:e.emitEvent}),this._updateAncestors(Object.assign(Object.assign({},e),{skipPristineCheck:t})),this._onDisabledChange.forEach(e=>e(!1))}_updateAncestors(e){this._parent&&!e.onlySelf&&(this._parent.updateValueAndValidity(e),e.skipPristineCheck||this._parent._updatePristine(),this._parent._updateTouched())}setParent(e){this._parent=e}updateValueAndValidity(e={}){this._setInitialStatus(),this._updateValue(),this.enabled&&(this._cancelExistingSubscription(),this.errors=this._runValidator(),this.status=this._calculateStatus(),this.status!==wS&&this.status!==kS||this._runAsyncValidator(e.emitEvent)),!1!==e.emitEvent&&(this.valueChanges.emit(this.value),this.statusChanges.emit(this.status)),this._parent&&!e.onlySelf&&this._parent.updateValueAndValidity(e)}_updateTreeValidity(e={emitEvent:!0}){this._forEachChild(t=>t._updateTreeValidity(e)),this.updateValueAndValidity({onlySelf:!0,emitEvent:e.emitEvent})}_setInitialStatus(){this.status=this._allControlsDisabled()?xS:wS}_runValidator(){return this.validator?this.validator(this):null}_runAsyncValidator(e){if(this.asyncValidator){this.status=kS,this._hasOwnPendingAsyncValidator=!0;const t=Jw(this.asyncValidator(this));this._asyncValidationSubscription=t.subscribe(t=>{this._hasOwnPendingAsyncValidator=!1,this.setErrors(t,{emitEvent:e})})}}_cancelExistingSubscription(){this._asyncValidationSubscription&&(this._asyncValidationSubscription.unsubscribe(),this._hasOwnPendingAsyncValidator=!1)}setErrors(e,t={}){this.errors=e,this._updateControlsErrors(!1!==t.emitEvent)}get(e){return function(e,t,i){if(null==t)return null;if(Array.isArray(t)||(t=t.split(".")),Array.isArray(t)&&0===t.length)return null;let n=e;return t.forEach(e=>{n=n instanceof PS?n.controls.hasOwnProperty(e)?n.controls[e]:null:n instanceof IS&&n.at(e)||null}),n}(this,e)}getError(e,t){const i=t?this.get(t):this;return i&&i.errors?i.errors[e]:null}hasError(e,t){return!!this.getError(e,t)}get root(){let e=this;for(;e._parent;)e=e._parent;return e}_updateControlsErrors(e){this.status=this._calculateStatus(),e&&this.statusChanges.emit(this.status),this._parent&&this._parent._updateControlsErrors(e)}_initObservables(){this.valueChanges=new Ul,this.statusChanges=new Ul}_calculateStatus(){return this._allControlsDisabled()?xS:this.errors?SS:this._hasOwnPendingAsyncValidator||this._anyControlsHaveStatus(kS)?kS:this._anyControlsHaveStatus(SS)?SS:wS}_anyControlsHaveStatus(e){return this._anyControls(t=>t.status===e)}_anyControlsDirty(){return this._anyControls(e=>e.dirty)}_anyControlsTouched(){return this._anyControls(e=>e.touched)}_updatePristine(e={}){this.pristine=!this._anyControlsDirty(),this._parent&&!e.onlySelf&&this._parent._updatePristine(e)}_updateTouched(e={}){this.touched=this._anyControlsTouched(),this._parent&&!e.onlySelf&&this._parent._updateTouched(e)}_isBoxedValue(e){return"object"==typeof e&&null!==e&&2===Object.keys(e).length&&"value"in e&&"disabled"in e}_registerOnCollectionChange(e){this._onCollectionChange=e}_setUpdateStrategy(e){RS(e)&&null!=e.updateOn&&(this._updateOn=e.updateOn)}_parentMarkedDirty(e){return!e&&!(!this._parent||!this._parent.dirty)&&!this._parent._anyControlsDirty()}}class DS extends LS{constructor(e=null,t,i){super(ES(t),TS(i,t)),this._onChange=[],this._applyFormState(e),this._setUpdateStrategy(t),this._initObservables(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!i})}setValue(e,t={}){this.value=this._pendingValue=e,this._onChange.length&&!1!==t.emitModelToViewChange&&this._onChange.forEach(e=>e(this.value,!1!==t.emitViewToModelChange)),this.updateValueAndValidity(t)}patchValue(e,t={}){this.setValue(e,t)}reset(e=null,t={}){this._applyFormState(e),this.markAsPristine(t),this.markAsUntouched(t),this.setValue(this.value,t),this._pendingChange=!1}_updateValue(){}_anyControls(e){return!1}_allControlsDisabled(){return this.disabled}registerOnChange(e){this._onChange.push(e)}_unregisterOnChange(e){CS(this._onChange,e)}registerOnDisabledChange(e){this._onDisabledChange.push(e)}_unregisterOnDisabledChange(e){CS(this._onDisabledChange,e)}_forEachChild(e){}_syncPendingControls(){return!("submit"!==this.updateOn||(this._pendingDirty&&this.markAsDirty(),this._pendingTouched&&this.markAsTouched(),!this._pendingChange)||(this.setValue(this._pendingValue,{onlySelf:!0,emitModelToViewChange:!1}),0))}_applyFormState(e){this._isBoxedValue(e)?(this.value=this._pendingValue=e.value,e.disabled?this.disable({onlySelf:!0,emitEvent:!1}):this.enable({onlySelf:!0,emitEvent:!1})):this.value=this._pendingValue=e}}class PS extends LS{constructor(e,t,i){super(ES(t),TS(i,t)),this.controls=e,this._initObservables(),this._setUpdateStrategy(t),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!i})}registerControl(e,t){return this.controls[e]?this.controls[e]:(this.controls[e]=t,t.setParent(this),t._registerOnCollectionChange(this._onCollectionChange),t)}addControl(e,t){this.registerControl(e,t),this.updateValueAndValidity(),this._onCollectionChange()}removeControl(e){this.controls[e]&&this.controls[e]._registerOnCollectionChange(()=>{}),delete this.controls[e],this.updateValueAndValidity(),this._onCollectionChange()}setControl(e,t){this.controls[e]&&this.controls[e]._registerOnCollectionChange(()=>{}),delete this.controls[e],t&&this.registerControl(e,t),this.updateValueAndValidity(),this._onCollectionChange()}contains(e){return this.controls.hasOwnProperty(e)&&this.controls[e].enabled}setValue(e,t={}){this._checkAllValuesPresent(e),Object.keys(e).forEach(i=>{this._throwIfControlMissing(i),this.controls[i].setValue(e[i],{onlySelf:!0,emitEvent:t.emitEvent})}),this.updateValueAndValidity(t)}patchValue(e,t={}){null!=e&&(Object.keys(e).forEach(i=>{this.controls[i]&&this.controls[i].patchValue(e[i],{onlySelf:!0,emitEvent:t.emitEvent})}),this.updateValueAndValidity(t))}reset(e={},t={}){this._forEachChild((i,n)=>{i.reset(e[n],{onlySelf:!0,emitEvent:t.emitEvent})}),this._updatePristine(t),this._updateTouched(t),this.updateValueAndValidity(t)}getRawValue(){return this._reduceChildren({},(e,t,i)=>(e[i]=t instanceof DS?t.value:t.getRawValue(),e))}_syncPendingControls(){let e=this._reduceChildren(!1,(e,t)=>!!t._syncPendingControls()||e);return e&&this.updateValueAndValidity({onlySelf:!0}),e}_throwIfControlMissing(e){if(!Object.keys(this.controls).length)throw new Error("\n There are no form controls registered with this group yet. If you're using ngModel,\n you may want to check next tick (e.g. use setTimeout).\n ");if(!this.controls[e])throw new Error(`Cannot find form control with name: ${e}.`)}_forEachChild(e){Object.keys(this.controls).forEach(t=>{const i=this.controls[t];i&&e(i,t)})}_setUpControls(){this._forEachChild(e=>{e.setParent(this),e._registerOnCollectionChange(this._onCollectionChange)})}_updateValue(){this.value=this._reduceValue()}_anyControls(e){for(const t of Object.keys(this.controls)){const i=this.controls[t];if(this.contains(t)&&e(i))return!0}return!1}_reduceValue(){return this._reduceChildren({},(e,t,i)=>((t.enabled||this.disabled)&&(e[i]=t.value),e))}_reduceChildren(e,t){let i=e;return this._forEachChild((e,n)=>{i=t(i,e,n)}),i}_allControlsDisabled(){for(const e of Object.keys(this.controls))if(this.controls[e].enabled)return!1;return Object.keys(this.controls).length>0||this.disabled}_checkAllValuesPresent(e){this._forEachChild((t,i)=>{if(void 0===e[i])throw new Error(`Must supply a value for form control with name: '${i}'.`)})}}class IS extends LS{constructor(e,t,i){super(ES(t),TS(i,t)),this.controls=e,this._initObservables(),this._setUpdateStrategy(t),this._setUpControls(),this.updateValueAndValidity({onlySelf:!0,emitEvent:!!i})}at(e){return this.controls[e]}push(e){this.controls.push(e),this._registerControl(e),this.updateValueAndValidity(),this._onCollectionChange()}insert(e,t){this.controls.splice(e,0,t),this._registerControl(t),this.updateValueAndValidity()}removeAt(e){this.controls[e]&&this.controls[e]._registerOnCollectionChange(()=>{}),this.controls.splice(e,1),this.updateValueAndValidity()}setControl(e,t){this.controls[e]&&this.controls[e]._registerOnCollectionChange(()=>{}),this.controls.splice(e,1),t&&(this.controls.splice(e,0,t),this._registerControl(t)),this.updateValueAndValidity(),this._onCollectionChange()}get length(){return this.controls.length}setValue(e,t={}){this._checkAllValuesPresent(e),e.forEach((e,i)=>{this._throwIfControlMissing(i),this.at(i).setValue(e,{onlySelf:!0,emitEvent:t.emitEvent})}),this.updateValueAndValidity(t)}patchValue(e,t={}){null!=e&&(e.forEach((e,i)=>{this.at(i)&&this.at(i).patchValue(e,{onlySelf:!0,emitEvent:t.emitEvent})}),this.updateValueAndValidity(t))}reset(e=[],t={}){this._forEachChild((i,n)=>{i.reset(e[n],{onlySelf:!0,emitEvent:t.emitEvent})}),this._updatePristine(t),this._updateTouched(t),this.updateValueAndValidity(t)}getRawValue(){return this.controls.map(e=>e instanceof DS?e.value:e.getRawValue())}clear(){this.controls.length<1||(this._forEachChild(e=>e._registerOnCollectionChange(()=>{})),this.controls.splice(0),this.updateValueAndValidity())}_syncPendingControls(){let e=this.controls.reduce((e,t)=>!!t._syncPendingControls()||e,!1);return e&&this.updateValueAndValidity({onlySelf:!0}),e}_throwIfControlMissing(e){if(!this.controls.length)throw new Error("\n There are no form controls registered with this array yet. If you're using ngModel,\n you may want to check next tick (e.g. use setTimeout).\n ");if(!this.at(e))throw new Error("Cannot find form control at index "+e)}_forEachChild(e){this.controls.forEach((t,i)=>{e(t,i)})}_updateValue(){this.value=this.controls.filter(e=>e.enabled||this.disabled).map(e=>e.value)}_anyControls(e){return this.controls.some(t=>t.enabled&&e(t))}_setUpControls(){this._forEachChild(e=>this._registerControl(e))}_checkAllValuesPresent(e){this._forEachChild((t,i)=>{if(void 0===e[i])throw new Error(`Must supply a value for form control at index: ${i}.`)})}_allControlsDisabled(){for(const e of this.controls)if(e.enabled)return!1;return this.controls.length>0||this.disabled}_registerControl(e){e.setParent(this),e._registerOnCollectionChange(this._onCollectionChange)}}const MS={provide:cS,useExisting:ae(()=>HS)},FS=(()=>Promise.resolve(null))();let HS=(()=>{class e extends cS{constructor(e,t){super(),this.submitted=!1,this._directives=[],this.ngSubmit=new Ul,this.form=new PS({},nS(e),rS(t))}ngAfterViewInit(){this._setUpdateStrategy()}get formDirective(){return this}get control(){return this.form}get path(){return[]}get controls(){return this.form.controls}addControl(e){FS.then(()=>{const t=this._findContainer(e.path);e.control=t.registerControl(e.name,e.control),fS(e.control,e),e.control.updateValueAndValidity({emitEvent:!1}),this._directives.push(e)})}getControl(e){return this.form.get(e.path)}removeControl(e){FS.then(()=>{const t=this._findContainer(e.path);t&&t.removeControl(e.name),CS(this._directives,e)})}addFormGroup(e){FS.then(()=>{const t=this._findContainer(e.path),i=new PS({});yS(i,e),t.registerControl(e.name,i),i.updateValueAndValidity({emitEvent:!1})})}removeFormGroup(e){FS.then(()=>{const t=this._findContainer(e.path);t&&t.removeControl(e.name)})}getFormGroup(e){return this.form.get(e.path)}updateModel(e,t){FS.then(()=>{this.form.get(e.path).setValue(t)})}setValue(e){this.control.setValue(e)}onSubmit(e){return this.submitted=!0,bS(this.form,this._directives),this.ngSubmit.emit(e),!1}onReset(){this.resetForm()}resetForm(e){this.form.reset(e),this.submitted=!1}_setUpdateStrategy(){this.options&&null!=this.options.updateOn&&(this.form._updateOn=this.options.updateOn)}_findContainer(e){return e.pop(),e.length?this.form.get(e):this.form}}return e.\u0275fac=function(t){return new(t||e)(xo(Yw,10),xo(Xw,10))},e.\u0275dir=Qe({type:e,selectors:[["form",3,"ngNoForm","",3,"formGroup",""],["ng-form"],["","ngForm",""]],hostBindings:function(e,t){1&e&&Ho("submit",(function(e){return t.onSubmit(e)}))("reset",(function(){return t.onReset()}))},inputs:{options:["ngFormOptions","options"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],features:[Da([MS]),lo]}),e})();const BS={provide:uS,useExisting:ae(()=>NS)},jS=(()=>Promise.resolve(null))();let NS=(()=>{class e extends uS{constructor(e,t,i,n){super(),this.control=new DS,this._registered=!1,this.update=new Ul,this._parent=e,this._setValidators(t),this._setAsyncValidators(i),this.valueAccessor=function(e,t){if(!t)return null;Array.isArray(t);let i=void 0,n=void 0,r=void 0;return t.forEach(e=>{e.constructor===Zw?i=e:Object.getPrototypeOf(e.constructor)===class{}?n=e:r=e}),r||n||i||null}(0,n)}ngOnChanges(e){this._checkForErrors(),this._registered||this._setUpControl(),"isDisabled"in e&&this._updateDisabled(e),function(e,t){if(!e.hasOwnProperty("model"))return!1;const i=e.model;return!!i.isFirstChange()||!Object.is(t,i.currentValue)}(e,this.viewModel)&&(this._updateValue(this.model),this.viewModel=this.model)}ngOnDestroy(){this.formDirective&&this.formDirective.removeControl(this)}get path(){return this._parent?[...this._parent.path,this.name]:[this.name]}get formDirective(){return this._parent?this._parent.formDirective:null}viewToModelUpdate(e){this.viewModel=e,this.update.emit(e)}_setUpControl(){this._setUpdateStrategy(),this._isStandalone()?this._setUpStandalone():this.formDirective.addControl(this),this._registered=!0}_setUpdateStrategy(){this.options&&null!=this.options.updateOn&&(this.control._updateOn=this.options.updateOn)}_isStandalone(){return!this._parent||!(!this.options||!this.options.standalone)}_setUpStandalone(){fS(this.control,this),this.control.updateValueAndValidity({emitEvent:!1})}_checkForErrors(){this._isStandalone()||this._checkParentType(),this._checkName()}_checkParentType(){}_checkName(){this.options&&this.options.name&&(this.name=this.options.name),this._isStandalone()}_updateValue(e){jS.then(()=>{this.control.setValue(e,{emitViewToModelChange:!1})})}_updateDisabled(e){const t=e.isDisabled.currentValue,i=""===t||t&&"false"!==t;jS.then(()=>{i&&!this.control.disabled?this.control.disable():!i&&this.control.disabled&&this.control.enable()})}}return e.\u0275fac=function(t){return new(t||e)(xo(cS,9),xo(Yw,10),xo(Xw,10),xo($w,10))},e.\u0275dir=Qe({type:e,selectors:[["","ngModel","",3,"formControlName","",3,"formControl",""]],inputs:{name:"name",isDisabled:["disabled","isDisabled"],model:["ngModel","model"],options:["ngModelOptions","options"]},outputs:{update:"ngModelChange"},exportAs:["ngModel"],features:[Da([BS]),lo,dt]}),e})(),VS=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})();const US=new Qi("NgModelWithFormControlWarning"),qS={provide:cS,useExisting:ae(()=>zS)};let zS=(()=>{class e extends cS{constructor(e,t){super(),this.validators=e,this.asyncValidators=t,this.submitted=!1,this._onCollectionChange=()=>this._updateDomValue(),this.directives=[],this.form=null,this.ngSubmit=new Ul,this._setValidators(e),this._setAsyncValidators(t)}ngOnChanges(e){this._checkFormPresent(),e.hasOwnProperty("form")&&(this._updateValidators(),this._updateDomValue(),this._updateRegistrations(),this._oldForm=this.form)}ngOnDestroy(){this.form&&(gS(this.form,this,!1),this.form._onCollectionChange===this._onCollectionChange&&this.form._registerOnCollectionChange(()=>{}))}get formDirective(){return this}get control(){return this.form}get path(){return[]}addControl(e){const t=this.form.get(e.path);return fS(t,e),t.updateValueAndValidity({emitEvent:!1}),this.directives.push(e),t}getControl(e){return this.form.get(e.path)}removeControl(e){pS(e.control||null,e,!1),CS(this.directives,e)}addFormGroup(e){this._setUpFormContainer(e)}removeFormGroup(e){this._cleanUpFormContainer(e)}getFormGroup(e){return this.form.get(e.path)}addFormArray(e){this._setUpFormContainer(e)}removeFormArray(e){this._cleanUpFormContainer(e)}getFormArray(e){return this.form.get(e.path)}updateModel(e,t){this.form.get(e.path).setValue(t)}onSubmit(e){return this.submitted=!0,bS(this.form,this.directives),this.ngSubmit.emit(e),!1}onReset(){this.resetForm()}resetForm(e){this.form.reset(e),this.submitted=!1}_updateDomValue(){this.directives.forEach(e=>{const t=e.control,i=this.form.get(e.path);t!==i&&(pS(t||null,e),i instanceof DS&&(fS(i,e),e.control=i))}),this.form._updateTreeValidity({emitEvent:!1})}_setUpFormContainer(e){const t=this.form.get(e.path);yS(t,e),t.updateValueAndValidity({emitEvent:!1})}_cleanUpFormContainer(e){if(this.form){const t=this.form.get(e.path);t&&function(e,t){return gS(e,t,!1)}(t,e)&&t.updateValueAndValidity({emitEvent:!1})}}_updateRegistrations(){this.form._registerOnCollectionChange(this._onCollectionChange),this._oldForm&&this._oldForm._registerOnCollectionChange(()=>{})}_updateValidators(){mS(this.form,this,!1),this._oldForm&&gS(this._oldForm,this,!1)}_checkFormPresent(){}}return e.\u0275fac=function(t){return new(t||e)(xo(Yw,10),xo(Xw,10))},e.\u0275dir=Qe({type:e,selectors:[["","formGroup",""]],hostBindings:function(e,t){1&e&&Ho("submit",(function(e){return t.onSubmit(e)}))("reset",(function(){return t.onReset()}))},inputs:{form:["formGroup","form"]},outputs:{ngSubmit:"ngSubmit"},exportAs:["ngForm"],features:[Da([qS]),lo,dt]}),e})(),WS=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[VS]]}),e})(),$S=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[WS]}),e})(),KS=(()=>{class e{static withConfig(t){return{ngModule:e,providers:[{provide:US,useValue:t.warnOnNgModelWithFormControl}]}}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[WS]}),e})();const GS=["trigger"],ZS=["panel"];function YS(e,t){if(1&e&&(To(0,"span",8),da(1),Oo()),2&e){const e=Uo();Zr(1),fa(e.placeholder)}}function XS(e,t){if(1&e&&(To(0,"span",12),da(1),Oo()),2&e){const e=Uo(2);Zr(1),fa(e.triggerValue)}}function QS(e,t){1&e&&Wo(0,0,["*ngSwitchCase","true"])}function JS(e,t){1&e&&(To(0,"span",9),So(1,XS,2,1,"span",10),So(2,QS,1,0,"ng-content",11),Oo()),2&e&&(Eo("ngSwitch",!!Uo().customTrigger),Zr(2),Eo("ngSwitchCase",!0))}function ek(e,t){if(1&e){const e=Po();To(0,"div",13),To(1,"div",14,15),Ho("@transformPanel.done",(function(t){return Bt(e),Uo()._panelDoneAnimatingStream.next(t.toState)}))("keydown",(function(t){return Bt(e),Uo()._handleKeydown(t)})),Wo(3,1),Oo(),Oo()}if(2&e){const e=Uo();Eo("@transformPanelWrap",void 0),Zr(1),"mat-select-panel ",i=e._getPanelTheme(),"",na(on,ta,wo(Ft(),"mat-select-panel ",i,""),!0),Qo("transform-origin",e._transformOrigin)("font-size",e._triggerFontSize,"px"),Eo("ngClass",e.panelClass)("@transformPanel",e.multiple?"showing-multiple":"showing"),Co("id",e.id+"-panel")("aria-multiselectable",e.multiple)("aria-label",e.ariaLabel||null)("aria-labelledby",e._getPanelAriaLabelledby())}var i}const tk=[[["mat-select-trigger"]],"*"],ik=["mat-select-trigger","*"],nk={transformPanelWrap:Eu("transformPanelWrap",[Lu("* => void",Pu("@transformPanel",[Du()],{optional:!0}))]),transformPanel:Eu("transformPanel",[Ru("void",Ou({transform:"scaleY(0.8)",minWidth:"100%",opacity:0})),Ru("showing",Ou({opacity:1,minWidth:"calc(100% + 32px)",transform:"scaleY(1)"})),Ru("showing-multiple",Ou({opacity:1,minWidth:"calc(100% + 64px)",transform:"scaleY(1)"})),Lu("void => *",Au("120ms cubic-bezier(0, 0, 0.2, 1)")),Lu("* => void",Au("100ms 25ms linear",Ou({opacity:0})))])};let rk=0;const sk=256,ok=new Qi("mat-select-scroll-strategy"),ak=new Qi("MAT_SELECT_CONFIG"),lk={provide:ok,deps:[ag],useFactory:function(e){return()=>e.scrollStrategies.reposition()}};class ck{constructor(e,t){this.source=e,this.value=t}}class hk{constructor(e,t,i,n,r){this._elementRef=e,this._defaultErrorStateMatcher=t,this._parentForm=i,this._parentFormGroup=n,this.ngControl=r}}const uk=$g(Kg(zg(Gg(hk)))),dk=new Qi("MatSelectTrigger");let fk=(()=>{class e extends uk{constructor(e,t,i,n,r,s,o,a,l,c,h,u,d,f){var p,_,m;super(r,n,o,a,c),this._viewportRuler=e,this._changeDetectorRef=t,this._ngZone=i,this._dir=s,this._parentFormField=l,this.ngControl=c,this._liveAnnouncer=d,this._defaultOptions=f,this._panelOpen=!1,this._compareWith=(e,t)=>e===t,this._uid="mat-select-"+rk++,this._triggerAriaLabelledBy=null,this._destroy=new S,this._onChange=()=>{},this._onTouched=()=>{},this._valueId="mat-select-value-"+rk++,this._panelDoneAnimatingStream=new S,this._overlayPanelClass=(null===(p=this._defaultOptions)||void 0===p?void 0:p.overlayPanelClass)||"",this._focused=!1,this.controlType="mat-select",this._required=!1,this._multiple=!1,this._disableOptionCentering=null!==(m=null===(_=this._defaultOptions)||void 0===_?void 0:_.disableOptionCentering)&&void 0!==m&&m,this.ariaLabel="",this.optionSelectionChanges=dp(()=>{const e=this.options;return e?e.changes.pipe(K_(e),Sp(()=>W(...e.map(e=>e.onSelectionChange)))):this._ngZone.onStable.pipe(Ap(1),Sp(()=>this.optionSelectionChanges))}),this.openedChange=new Ul,this._openedStream=this.openedChange.pipe(bp(e=>e),M(()=>{})),this._closedStream=this.openedChange.pipe(bp(e=>!e),M(()=>{})),this.selectionChange=new Ul,this.valueChange=new Ul,this.ngControl&&(this.ngControl.valueAccessor=this),null!=(null==f?void 0:f.typeaheadDebounceInterval)&&(this._typeaheadDebounceInterval=f.typeaheadDebounceInterval),this._scrollStrategyFactory=u,this._scrollStrategy=this._scrollStrategyFactory(),this.tabIndex=parseInt(h)||0,this.id=this.id}get focused(){return this._focused||this._panelOpen}get placeholder(){return this._placeholder}set placeholder(e){this._placeholder=e,this.stateChanges.next()}get required(){return this._required}set required(e){this._required=D_(e),this.stateChanges.next()}get multiple(){return this._multiple}set multiple(e){this._multiple=D_(e)}get disableOptionCentering(){return this._disableOptionCentering}set disableOptionCentering(e){this._disableOptionCentering=D_(e)}get compareWith(){return this._compareWith}set compareWith(e){this._compareWith=e,this._selectionModel&&this._initializeSelection()}get value(){return this._value}set value(e){(e!==this._value||this._multiple&&Array.isArray(e))&&(this.options&&this._setSelectionByValue(e),this._value=e)}get typeaheadDebounceInterval(){return this._typeaheadDebounceInterval}set typeaheadDebounceInterval(e){this._typeaheadDebounceInterval=P_(e)}get id(){return this._id}set id(e){this._id=e||this._uid,this.stateChanges.next()}ngOnInit(){this._selectionModel=new gm(this.multiple),this.stateChanges.next(),this._panelDoneAnimatingStream.pipe(e=>e.lift(new H_(void 0,void 0)),z_(this._destroy)).subscribe(()=>this._panelDoneAnimating(this.panelOpen))}ngAfterContentInit(){this._initKeyManager(),this._selectionModel.changed.pipe(z_(this._destroy)).subscribe(e=>{e.added.forEach(e=>e.select()),e.removed.forEach(e=>e.deselect())}),this.options.changes.pipe(K_(null),z_(this._destroy)).subscribe(()=>{this._resetOptions(),this._initializeSelection()})}ngDoCheck(){const e=this._getTriggerAriaLabelledby();if(e!==this._triggerAriaLabelledBy){const t=this._elementRef.nativeElement;this._triggerAriaLabelledBy=e,e?t.setAttribute("aria-labelledby",e):t.removeAttribute("aria-labelledby")}this.ngControl&&this.updateErrorState()}ngOnChanges(e){e.disabled&&this.stateChanges.next(),e.typeaheadDebounceInterval&&this._keyManager&&this._keyManager.withTypeAhead(this._typeaheadDebounceInterval)}ngOnDestroy(){this._destroy.next(),this._destroy.complete(),this.stateChanges.complete()}toggle(){this.panelOpen?this.close():this.open()}open(){this._canOpen()&&(this._panelOpen=!0,this._keyManager.withHorizontalOrientation(null),this._highlightCorrectOption(),this._changeDetectorRef.markForCheck())}close(){this._panelOpen&&(this._panelOpen=!1,this._keyManager.withHorizontalOrientation(this._isRtl()?"rtl":"ltr"),this._changeDetectorRef.markForCheck(),this._onTouched())}writeValue(e){this.value=e}registerOnChange(e){this._onChange=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e,this._changeDetectorRef.markForCheck(),this.stateChanges.next()}get panelOpen(){return this._panelOpen}get selected(){return this.multiple?this._selectionModel.selected:this._selectionModel.selected[0]}get triggerValue(){if(this.empty)return"";if(this._multiple){const e=this._selectionModel.selected.map(e=>e.viewValue);return this._isRtl()&&e.reverse(),e.join(", ")}return this._selectionModel.selected[0].viewValue}_isRtl(){return!!this._dir&&"rtl"===this._dir.value}_handleKeydown(e){this.disabled||(this.panelOpen?this._handleOpenKeydown(e):this._handleClosedKeydown(e))}_handleClosedKeydown(e){const t=e.keyCode,i=40===t||38===t||37===t||39===t,n=13===t||32===t,r=this._keyManager;if(!r.isTyping()&&n&&!Dm(e)||(this.multiple||e.altKey)&&i)e.preventDefault(),this.open();else if(!this.multiple){const t=this.selected;r.onKeydown(e);const i=this.selected;i&&t!==i&&this._liveAnnouncer.announce(i.viewValue,1e4)}}_handleOpenKeydown(e){const t=this._keyManager,i=e.keyCode,n=40===i||38===i,r=t.isTyping();if(n&&e.altKey)e.preventDefault(),this.close();else if(r||13!==i&&32!==i||!t.activeItem||Dm(e))if(!r&&this._multiple&&65===i&&e.ctrlKey){e.preventDefault();const t=this.options.some(e=>!e.disabled&&!e.selected);this.options.forEach(e=>{e.disabled||(t?e.select():e.deselect())})}else{const i=t.activeItemIndex;t.onKeydown(e),this._multiple&&n&&e.shiftKey&&t.activeItem&&t.activeItemIndex!==i&&t.activeItem._selectViaInteraction()}else e.preventDefault(),t.activeItem._selectViaInteraction()}_onFocus(){this.disabled||(this._focused=!0,this.stateChanges.next())}_onBlur(){this._focused=!1,this.disabled||this.panelOpen||(this._onTouched(),this._changeDetectorRef.markForCheck(),this.stateChanges.next())}_onAttached(){this.overlayDir.positionChange.pipe(Ap(1)).subscribe(()=>{this._changeDetectorRef.detectChanges(),this._positioningSettled()})}_getPanelTheme(){return this._parentFormField?"mat-"+this._parentFormField.color:""}get empty(){return!this._selectionModel||this._selectionModel.isEmpty()}_initializeSelection(){Promise.resolve().then(()=>{this._setSelectionByValue(this.ngControl?this.ngControl.value:this._value),this.stateChanges.next()})}_setSelectionByValue(e){if(this._selectionModel.selected.forEach(e=>e.setInactiveStyles()),this._selectionModel.clear(),this.multiple&&e)Array.isArray(e),e.forEach(e=>this._selectValue(e)),this._sortValues();else{const t=this._selectValue(e);t?this._keyManager.updateActiveItem(t):this.panelOpen||this._keyManager.updateActiveItem(-1)}this._changeDetectorRef.markForCheck()}_selectValue(e){const t=this.options.find(t=>{if(this._selectionModel.isSelected(t))return!1;try{return null!=t.value&&this._compareWith(t.value,e)}catch(i){return!1}});return t&&this._selectionModel.select(t),t}_initKeyManager(){this._keyManager=new wg(this.options).withTypeAhead(this._typeaheadDebounceInterval).withVerticalOrientation().withHorizontalOrientation(this._isRtl()?"rtl":"ltr").withHomeAndEnd().withAllowedModifierKeys(["shiftKey"]),this._keyManager.tabOut.pipe(z_(this._destroy)).subscribe(()=>{this.panelOpen&&(!this.multiple&&this._keyManager.activeItem&&this._keyManager.activeItem._selectViaInteraction(),this.focus(),this.close())}),this._keyManager.change.pipe(z_(this._destroy)).subscribe(()=>{this._panelOpen&&this.panel?this._scrollOptionIntoView(this._keyManager.activeItemIndex||0):this._panelOpen||this.multiple||!this._keyManager.activeItem||this._keyManager.activeItem._selectViaInteraction()})}_resetOptions(){const e=W(this.options.changes,this._destroy);this.optionSelectionChanges.pipe(z_(e)).subscribe(e=>{this._onSelect(e.source,e.isUserInput),e.isUserInput&&!this.multiple&&this._panelOpen&&(this.close(),this.focus())}),W(...this.options.map(e=>e._stateChanges)).pipe(z_(e)).subscribe(()=>{this._changeDetectorRef.markForCheck(),this.stateChanges.next()})}_onSelect(e,t){const i=this._selectionModel.isSelected(e);null!=e.value||this._multiple?(i!==e.selected&&(e.selected?this._selectionModel.select(e):this._selectionModel.deselect(e)),t&&this._keyManager.setActiveItem(e),this.multiple&&(this._sortValues(),t&&this.focus())):(e.deselect(),this._selectionModel.clear(),null!=this.value&&this._propagateChanges(e.value)),i!==this._selectionModel.isSelected(e)&&this._propagateChanges(),this.stateChanges.next()}_sortValues(){if(this.multiple){const e=this.options.toArray();this._selectionModel.sort((t,i)=>this.sortComparator?this.sortComparator(t,i,e):e.indexOf(t)-e.indexOf(i)),this.stateChanges.next()}}_propagateChanges(e){let t=null;t=this.multiple?this.selected.map(e=>e.value):this.selected?this.selected.value:e,this._value=t,this.valueChange.emit(t),this._onChange(t),this.selectionChange.emit(this._getChangeEvent(t)),this._changeDetectorRef.markForCheck()}_highlightCorrectOption(){this._keyManager&&(this.empty?this._keyManager.setFirstItemActive():this._keyManager.setActiveItem(this._selectionModel.selected[0]))}_canOpen(){var e;return!this._panelOpen&&!this.disabled&&(null===(e=this.options)||void 0===e?void 0:e.length)>0}focus(e){this._elementRef.nativeElement.focus(e)}_getPanelAriaLabelledby(){var e;if(this.ariaLabel)return null;const t=null===(e=this._parentFormField)||void 0===e?void 0:e.getLabelId();return this.ariaLabelledby?(t?t+" ":"")+this.ariaLabelledby:t}_getAriaActiveDescendant(){return this.panelOpen&&this._keyManager&&this._keyManager.activeItem?this._keyManager.activeItem.id:null}_getTriggerAriaLabelledby(){var e;if(this.ariaLabel)return null;const t=null===(e=this._parentFormField)||void 0===e?void 0:e.getLabelId();let i=(t?t+" ":"")+this._valueId;return this.ariaLabelledby&&(i+=" "+this.ariaLabelledby),i}_panelDoneAnimating(e){this.openedChange.emit(e)}setDescribedByIds(e){this._ariaDescribedby=e.join(" ")}onContainerClick(){this.focus(),this.open()}get shouldLabelFloat(){return this._panelOpen||!this.empty||this._focused&&!!this._placeholder}}return e.\u0275fac=function(t){return new(t||e)(xo(ym),xo(hl),xo(Tc),xo(Zg),xo(ja),xo(_m,8),xo(HS,8),xo(zS,8),xo(Uw,8),xo(uS,10),Zi("tabindex"),xo(ok),xo(xg),xo(ak,8))},e.\u0275dir=Qe({type:e,viewQuery:function(e,t){if(1&e&&(ec(GS,1),ec(ZS,1),ec(ug,1)),2&e){let e;Jl(e=ic())&&(t.trigger=e.first),Jl(e=ic())&&(t.panel=e.first),Jl(e=ic())&&(t.overlayDir=e.first)}},inputs:{ariaLabel:["aria-label","ariaLabel"],id:"id",placeholder:"placeholder",required:"required",multiple:"multiple",disableOptionCentering:"disableOptionCentering",compareWith:"compareWith",value:"value",typeaheadDebounceInterval:"typeaheadDebounceInterval",panelClass:"panelClass",ariaLabelledby:["aria-labelledby","ariaLabelledby"],errorStateMatcher:"errorStateMatcher",sortComparator:"sortComparator"},outputs:{openedChange:"openedChange",_openedStream:"opened",_closedStream:"closed",selectionChange:"selectionChange",valueChange:"valueChange"},features:[lo,dt]}),e})(),pk=(()=>{class e extends fk{constructor(){super(...arguments),this._scrollTop=0,this._triggerFontSize=0,this._transformOrigin="top",this._offsetY=0,this._positions=[{originX:"start",originY:"top",overlayX:"start",overlayY:"top"},{originX:"start",originY:"bottom",overlayX:"start",overlayY:"bottom"}]}_calculateOverlayScroll(e,t,i){const n=this._getItemHeight();return Math.min(Math.max(0,n*e-t+n/2),i)}ngOnInit(){super.ngOnInit(),this._viewportRuler.change().pipe(z_(this._destroy)).subscribe(()=>{this.panelOpen&&(this._triggerRect=this.trigger.nativeElement.getBoundingClientRect(),this._changeDetectorRef.markForCheck())})}open(){super._canOpen()&&(super.open(),this._triggerRect=this.trigger.nativeElement.getBoundingClientRect(),this._triggerFontSize=parseInt(getComputedStyle(this.trigger.nativeElement).fontSize||"0"),this._calculateOverlayPosition(),this._ngZone.onStable.pipe(Ap(1)).subscribe(()=>{this._triggerFontSize&&this.overlayDir.overlayRef&&this.overlayDir.overlayRef.overlayElement&&(this.overlayDir.overlayRef.overlayElement.style.fontSize=this._triggerFontSize+"px")}))}_scrollOptionIntoView(e){const t=gv(e,this.options,this.optionGroups),i=this._getItemHeight();var n,r,s;this.panel.nativeElement.scrollTop=(r=i,sk,(n=(e+t)*i)<(s=this.panel.nativeElement.scrollTop)?n:n+r>s+256?Math.max(0,n-256+r):s)}_positioningSettled(){this._calculateOverlayOffsetX(),this.panel.nativeElement.scrollTop=this._scrollTop}_panelDoneAnimating(e){this.panelOpen?this._scrollTop=0:(this.overlayDir.offsetX=0,this._changeDetectorRef.markForCheck()),super._panelDoneAnimating(e)}_getChangeEvent(e){return new ck(this,e)}_calculateOverlayOffsetX(){const e=this.overlayDir.overlayRef.overlayElement.getBoundingClientRect(),t=this._viewportRuler.getViewportSize(),i=this._isRtl(),n=this.multiple?56:32;let r;if(this.multiple)r=40;else if(this.disableOptionCentering)r=16;else{let e=this._selectionModel.selected[0]||this.options.first;r=e&&e.group?32:16}i||(r*=-1);const s=0-(e.left+r-(i?n:0)),o=e.right+r-t.width+(i?0:n);s>0?r+=s+8:o>0&&(r-=o+8),this.overlayDir.offsetX=Math.round(r),this.overlayDir.overlayRef.updatePosition()}_calculateOverlayOffsetY(e,t,i){const n=this._getItemHeight(),r=(n-this._triggerRect.height)/2,s=Math.floor(sk/n);let o;return this.disableOptionCentering?0:(o=0===this._scrollTop?e*n:this._scrollTop===i?(e-(this._getItemCount()-s))*n+(n-(this._getItemCount()*n-sk)%n):t-n/2,Math.round(-1*o-r))}_checkOverlayWithinViewport(e){const t=this._getItemHeight(),i=this._viewportRuler.getViewportSize(),n=this._triggerRect.top-8,r=i.height-this._triggerRect.bottom-8,s=Math.abs(this._offsetY),o=Math.min(this._getItemCount()*t,sk)-s-this._triggerRect.height;o>r?this._adjustPanelUp(o,r):s>n?this._adjustPanelDown(s,n,e):this._transformOrigin=this._getOriginBasedOnOption()}_adjustPanelUp(e,t){const i=Math.round(e-t);this._scrollTop-=i,this._offsetY-=i,this._transformOrigin=this._getOriginBasedOnOption(),this._scrollTop<=0&&(this._scrollTop=0,this._offsetY=0,this._transformOrigin="50% bottom 0px")}_adjustPanelDown(e,t,i){const n=Math.round(e-t);if(this._scrollTop+=n,this._offsetY+=n,this._transformOrigin=this._getOriginBasedOnOption(),this._scrollTop>=i)return this._scrollTop=i,this._offsetY=0,void(this._transformOrigin="50% top 0px")}_calculateOverlayPosition(){const e=this._getItemHeight(),t=this._getItemCount(),i=Math.min(t*e,sk),n=t*e-i;let r;r=this.empty?0:Math.max(this.options.toArray().indexOf(this._selectionModel.selected[0]),0),r+=gv(r,this.options,this.optionGroups);const s=i/2;this._scrollTop=this._calculateOverlayScroll(r,s,n),this._offsetY=this._calculateOverlayOffsetY(r,s,n),this._checkOverlayWithinViewport(n)}_getOriginBasedOnOption(){const e=this._getItemHeight(),t=(e-this._triggerRect.height)/2;return`50% ${Math.abs(this._offsetY)-t+e/2}px 0px`}_getItemHeight(){return 3*this._triggerFontSize}_getItemCount(){return this.options.length+this.optionGroups.length}}return e.\u0275fac=function(t){return _k(t||e)},e.\u0275cmp=$e({type:e,selectors:[["mat-select"]],contentQueries:function(e,t,i){if(1&e&&(tc(i,dk,1),tc(i,mv,1),tc(i,dv,1)),2&e){let e;Jl(e=ic())&&(t.customTrigger=e.first),Jl(e=ic())&&(t.options=e),Jl(e=ic())&&(t.optionGroups=e)}},hostAttrs:["role","combobox","aria-autocomplete","none","aria-haspopup","true",1,"mat-select"],hostVars:20,hostBindings:function(e,t){1&e&&Ho("keydown",(function(e){return t._handleKeydown(e)}))("focus",(function(){return t._onFocus()}))("blur",(function(){return t._onBlur()})),2&e&&(Co("id",t.id)("tabindex",t.tabIndex)("aria-controls",t.panelOpen?t.id+"-panel":null)("aria-expanded",t.panelOpen)("aria-label",t.ariaLabel||null)("aria-required",t.required.toString())("aria-disabled",t.disabled.toString())("aria-invalid",t.errorState)("aria-describedby",t._ariaDescribedby||null)("aria-activedescendant",t._getAriaActiveDescendant()),Jo("mat-select-disabled",t.disabled)("mat-select-invalid",t.errorState)("mat-select-required",t.required)("mat-select-empty",t.empty)("mat-select-multiple",t.multiple))},inputs:{disabled:"disabled",disableRipple:"disableRipple",tabIndex:"tabIndex"},exportAs:["matSelect"],features:[Da([{provide:Lw,useExisting:e},{provide:av,useExisting:e}]),lo],ngContentSelectors:ik,decls:9,vars:12,consts:[["cdk-overlay-origin","",1,"mat-select-trigger",3,"click"],["origin","cdkOverlayOrigin","trigger",""],[1,"mat-select-value",3,"ngSwitch"],["class","mat-select-placeholder mat-select-min-line",4,"ngSwitchCase"],["class","mat-select-value-text",3,"ngSwitch",4,"ngSwitchCase"],[1,"mat-select-arrow-wrapper"],[1,"mat-select-arrow"],["cdk-connected-overlay","","cdkConnectedOverlayLockPosition","","cdkConnectedOverlayHasBackdrop","","cdkConnectedOverlayBackdropClass","cdk-overlay-transparent-backdrop",3,"cdkConnectedOverlayPanelClass","cdkConnectedOverlayScrollStrategy","cdkConnectedOverlayOrigin","cdkConnectedOverlayOpen","cdkConnectedOverlayPositions","cdkConnectedOverlayMinWidth","cdkConnectedOverlayOffsetY","backdropClick","attach","detach"],[1,"mat-select-placeholder","mat-select-min-line"],[1,"mat-select-value-text",3,"ngSwitch"],["class","mat-select-min-line",4,"ngSwitchDefault"],[4,"ngSwitchCase"],[1,"mat-select-min-line"],[1,"mat-select-panel-wrap"],["role","listbox","tabindex","-1",3,"ngClass","keydown"],["panel",""]],template:function(e,t){if(1&e&&(zo(tk),To(0,"div",0,1),Ho("click",(function(){return t.toggle()})),To(3,"div",2),So(4,YS,2,1,"span",3),So(5,JS,3,2,"span",4),Oo(),To(6,"div",5),Ro(7,"div",6),Oo(),Oo(),So(8,ek,4,14,"ng-template",7),Ho("backdropClick",(function(){return t.close()}))("attach",(function(){return t._onAttached()}))("detach",(function(){return t.close()}))),2&e){const e=ko(1);Co("aria-owns",t.panelOpen?t.id+"-panel":null),Zr(3),Eo("ngSwitch",t.empty),Co("id",t._valueId),Zr(1),Eo("ngSwitchCase",!0),Zr(1),Eo("ngSwitchCase",!1),Zr(3),Eo("cdkConnectedOverlayPanelClass",t._overlayPanelClass)("cdkConnectedOverlayScrollStrategy",t._scrollStrategy)("cdkConnectedOverlayOrigin",e)("cdkConnectedOverlayOpen",t.panelOpen)("cdkConnectedOverlayPositions",t._positions)("cdkConnectedOverlayMinWidth",null==t._triggerRect?null:t._triggerRect.width)("cdkConnectedOverlayOffsetY",t._offsetY)}},directives:[hg,Hh,Bh,ug,jh,Oh],styles:['.mat-select{display:inline-block;width:100%;outline:none}.mat-select-trigger{display:inline-table;cursor:pointer;position:relative;box-sizing:border-box}.mat-select-disabled .mat-select-trigger{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.mat-select-value{display:table-cell;max-width:0;width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mat-select-value-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mat-select-arrow-wrapper{display:table-cell;vertical-align:middle}.mat-form-field-appearance-fill .mat-select-arrow-wrapper{transform:translateY(-50%)}.mat-form-field-appearance-outline .mat-select-arrow-wrapper{transform:translateY(-25%)}.mat-form-field-appearance-standard.mat-form-field-has-label .mat-select:not(.mat-select-empty) .mat-select-arrow-wrapper{transform:translateY(-50%)}.mat-form-field-appearance-standard .mat-select.mat-select-empty .mat-select-arrow-wrapper{transition:transform 400ms cubic-bezier(0.25, 0.8, 0.25, 1)}._mat-animation-noopable.mat-form-field-appearance-standard .mat-select.mat-select-empty .mat-select-arrow-wrapper{transition:none}.mat-select-arrow{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid;margin:0 4px}.mat-select-panel-wrap{flex-basis:100%}.mat-select-panel{min-width:112px;max-width:280px;overflow:auto;-webkit-overflow-scrolling:touch;padding-top:0;padding-bottom:0;max-height:256px;min-width:100%;border-radius:4px;outline:0}.cdk-high-contrast-active .mat-select-panel{outline:solid 1px}.mat-select-panel .mat-optgroup-label,.mat-select-panel .mat-option{font-size:inherit;line-height:3em;height:3em}.mat-form-field-type-mat-select:not(.mat-form-field-disabled) .mat-form-field-flex{cursor:pointer}.mat-form-field-type-mat-select .mat-form-field-label{width:calc(100% - 18px)}.mat-select-placeholder{transition:color 400ms 133.3333333333ms cubic-bezier(0.25, 0.8, 0.25, 1)}._mat-animation-noopable .mat-select-placeholder{transition:none}.mat-form-field-hide-placeholder .mat-select-placeholder{color:transparent;-webkit-text-fill-color:transparent;transition:none;display:block}.mat-select-min-line:empty::before{content:" ";white-space:pre;width:1px}\n'],encapsulation:2,data:{animation:[nk.transformPanelWrap,nk.transformPanel]},changeDetection:0}),e})();const _k=Ki(pk);let mk=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[lk],imports:[[Wh,fg,vv,qg],bm,zw,vv,qg]}),e})();const gk=["input"],vk=function(){return{enterDuration:150}},yk=["*"],bk=new Qi("mat-checkbox-default-options",{providedIn:"root",factory:Ck});function Ck(){return{color:"accent",clickAction:"check-indeterminate"}}let wk=0;const Sk=Ck(),kk={provide:$w,useExisting:ae(()=>Tk),multi:!0};class xk{}class Ek{constructor(e){this._elementRef=e}}const Ak=Kg(Wg($g(zg(Ek))));let Tk=(()=>{class e extends Ak{constructor(e,t,i,n,r,s,o){super(e),this._changeDetectorRef=t,this._focusMonitor=i,this._ngZone=n,this._animationMode=s,this._options=o,this.ariaLabel="",this.ariaLabelledby=null,this._uniqueId="mat-checkbox-"+ ++wk,this.id=this._uniqueId,this.labelPosition="after",this.name=null,this.change=new Ul,this.indeterminateChange=new Ul,this._onTouched=()=>{},this._currentAnimationClass="",this._currentCheckState=0,this._controlValueAccessorChangeFn=()=>{},this._checked=!1,this._disabled=!1,this._indeterminate=!1,this._options=this._options||Sk,this.color=this.defaultColor=this._options.color||Sk.color,this.tabIndex=parseInt(r)||0}get inputId(){return(this.id||this._uniqueId)+"-input"}get required(){return this._required}set required(e){this._required=D_(e)}ngAfterViewInit(){this._focusMonitor.monitor(this._elementRef,!0).subscribe(e=>{e||Promise.resolve().then(()=>{this._onTouched(),this._changeDetectorRef.markForCheck()})}),this._syncIndeterminate(this._indeterminate)}ngAfterViewChecked(){}ngOnDestroy(){this._focusMonitor.stopMonitoring(this._elementRef)}get checked(){return this._checked}set checked(e){e!=this.checked&&(this._checked=e,this._changeDetectorRef.markForCheck())}get disabled(){return this._disabled}set disabled(e){const t=D_(e);t!==this.disabled&&(this._disabled=t,this._changeDetectorRef.markForCheck())}get indeterminate(){return this._indeterminate}set indeterminate(e){const t=e!=this._indeterminate;this._indeterminate=D_(e),t&&(this._transitionCheckState(this._indeterminate?3:this.checked?1:2),this.indeterminateChange.emit(this._indeterminate)),this._syncIndeterminate(this._indeterminate)}_isRippleDisabled(){return this.disableRipple||this.disabled}_onLabelTextChange(){this._changeDetectorRef.detectChanges()}writeValue(e){this.checked=!!e}registerOnChange(e){this._controlValueAccessorChangeFn=e}registerOnTouched(e){this._onTouched=e}setDisabledState(e){this.disabled=e}_getAriaChecked(){return this.checked?"true":this.indeterminate?"mixed":"false"}_transitionCheckState(e){let t=this._currentCheckState,i=this._elementRef.nativeElement;if(t!==e&&(this._currentAnimationClass.length>0&&i.classList.remove(this._currentAnimationClass),this._currentAnimationClass=this._getAnimationClassForCheckStateTransition(t,e),this._currentCheckState=e,this._currentAnimationClass.length>0)){i.classList.add(this._currentAnimationClass);const e=this._currentAnimationClass;this._ngZone.runOutsideAngular(()=>{setTimeout(()=>{i.classList.remove(e)},1e3)})}}_emitChangeEvent(){const e=new xk;e.source=this,e.checked=this.checked,this._controlValueAccessorChangeFn(this.checked),this.change.emit(e)}toggle(){this.checked=!this.checked}_onInputClick(e){var t;const i=null===(t=this._options)||void 0===t?void 0:t.clickAction;e.stopPropagation(),this.disabled||"noop"===i?this.disabled||"noop"!==i||(this._inputElement.nativeElement.checked=this.checked,this._inputElement.nativeElement.indeterminate=this.indeterminate):(this.indeterminate&&"check"!==i&&Promise.resolve().then(()=>{this._indeterminate=!1,this.indeterminateChange.emit(this._indeterminate)}),this.toggle(),this._transitionCheckState(this._checked?1:2),this._emitChangeEvent())}focus(e,t){e?this._focusMonitor.focusVia(this._inputElement,e,t):this._inputElement.nativeElement.focus(t)}_onInteractionEvent(e){e.stopPropagation()}_getAnimationClassForCheckStateTransition(e,t){if("NoopAnimations"===this._animationMode)return"";let i="";switch(e){case 0:if(1===t)i="unchecked-checked";else{if(3!=t)return"";i="unchecked-indeterminate"}break;case 2:i=1===t?"unchecked-checked":"unchecked-indeterminate";break;case 1:i=2===t?"checked-unchecked":"checked-indeterminate";break;case 3:i=1===t?"indeterminate-checked":"indeterminate-unchecked"}return"mat-checkbox-anim-"+i}_syncIndeterminate(e){const t=this._inputElement;t&&(t.nativeElement.indeterminate=e)}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(hl),xo(Rg),xo(Tc),Zi("tabindex"),xo(ap,8),xo(bk,8))},e.\u0275cmp=$e({type:e,selectors:[["mat-checkbox"]],viewQuery:function(e,t){if(1&e&&(ec(gk,1),ec(nv,1)),2&e){let e;Jl(e=ic())&&(t._inputElement=e.first),Jl(e=ic())&&(t.ripple=e.first)}},hostAttrs:[1,"mat-checkbox"],hostVars:12,hostBindings:function(e,t){2&e&&(_a("id",t.id),Co("tabindex",null),Jo("mat-checkbox-indeterminate",t.indeterminate)("mat-checkbox-checked",t.checked)("mat-checkbox-disabled",t.disabled)("mat-checkbox-label-before","before"==t.labelPosition)("_mat-animation-noopable","NoopAnimations"===t._animationMode))},inputs:{disableRipple:"disableRipple",color:"color",tabIndex:"tabIndex",ariaLabel:["aria-label","ariaLabel"],ariaLabelledby:["aria-labelledby","ariaLabelledby"],id:"id",labelPosition:"labelPosition",name:"name",required:"required",checked:"checked",disabled:"disabled",indeterminate:"indeterminate",ariaDescribedby:["aria-describedby","ariaDescribedby"],value:"value"},outputs:{change:"change",indeterminateChange:"indeterminateChange"},exportAs:["matCheckbox"],features:[Da([kk]),lo],ngContentSelectors:yk,decls:17,vars:20,consts:[[1,"mat-checkbox-layout"],["label",""],[1,"mat-checkbox-inner-container"],["type","checkbox",1,"mat-checkbox-input","cdk-visually-hidden",3,"id","required","checked","disabled","tabIndex","change","click"],["input",""],["matRipple","",1,"mat-checkbox-ripple","mat-focus-indicator",3,"matRippleTrigger","matRippleDisabled","matRippleRadius","matRippleCentered","matRippleAnimation"],[1,"mat-ripple-element","mat-checkbox-persistent-ripple"],[1,"mat-checkbox-frame"],[1,"mat-checkbox-background"],["version","1.1","focusable","false","viewBox","0 0 24 24",0,"xml","space","preserve",1,"mat-checkbox-checkmark"],["fill","none","stroke","white","d","M4.1,12.7 9,17.6 20.3,6.3",1,"mat-checkbox-checkmark-path"],[1,"mat-checkbox-mixedmark"],[1,"mat-checkbox-label",3,"cdkObserveContent"],["checkboxLabel",""],[2,"display","none"]],template:function(e,t){if(1&e&&(zo(),To(0,"label",0,1),To(2,"span",2),To(3,"input",3,4),Ho("change",(function(e){return t._onInteractionEvent(e)}))("click",(function(e){return t._onInputClick(e)})),Oo(),To(5,"span",5),Ro(6,"span",6),Oo(),Ro(7,"span",7),To(8,"span",8),ui(),To(9,"svg",9),Ro(10,"path",10),Oo(),It.lFrame.currentNamespace=null,Ro(11,"span",11),Oo(),Oo(),To(12,"span",12,13),Ho("cdkObserveContent",(function(){return t._onLabelTextChange()})),To(14,"span",14),da(15,"\xa0"),Oo(),Wo(16),Oo(),Oo()),2&e){const e=ko(1),i=ko(13);Co("for",t.inputId),Zr(2),Jo("mat-checkbox-inner-container-no-side-margin",!i.textContent||!i.textContent.trim()),Zr(1),Eo("id",t.inputId)("required",t.required)("checked",t.checked)("disabled",t.disabled)("tabIndex",t.tabIndex),Co("value",t.value)("name",t.name)("aria-label",t.ariaLabel||null)("aria-labelledby",t.ariaLabelledby)("aria-checked",t._getAriaChecked())("aria-describedby",t.ariaDescribedby),Zr(2),Eo("matRippleTrigger",e)("matRippleDisabled",t._isRippleDisabled())("matRippleRadius",20)("matRippleCentered",!0)("matRippleAnimation",Bl(19,vk))}},directives:[nv,bg],styles:["@keyframes mat-checkbox-fade-in-background{0%{opacity:0}50%{opacity:1}}@keyframes mat-checkbox-fade-out-background{0%,50%{opacity:1}100%{opacity:0}}@keyframes mat-checkbox-unchecked-checked-checkmark-path{0%,50%{stroke-dashoffset:22.910259}50%{animation-timing-function:cubic-bezier(0, 0, 0.2, 0.1)}100%{stroke-dashoffset:0}}@keyframes mat-checkbox-unchecked-indeterminate-mixedmark{0%,68.2%{transform:scaleX(0)}68.2%{animation-timing-function:cubic-bezier(0, 0, 0, 1)}100%{transform:scaleX(1)}}@keyframes mat-checkbox-checked-unchecked-checkmark-path{from{animation-timing-function:cubic-bezier(0.4, 0, 1, 1);stroke-dashoffset:0}to{stroke-dashoffset:-22.910259}}@keyframes mat-checkbox-checked-indeterminate-checkmark{from{animation-timing-function:cubic-bezier(0, 0, 0.2, 0.1);opacity:1;transform:rotate(0deg)}to{opacity:0;transform:rotate(45deg)}}@keyframes mat-checkbox-indeterminate-checked-checkmark{from{animation-timing-function:cubic-bezier(0.14, 0, 0, 1);opacity:0;transform:rotate(45deg)}to{opacity:1;transform:rotate(360deg)}}@keyframes mat-checkbox-checked-indeterminate-mixedmark{from{animation-timing-function:cubic-bezier(0, 0, 0.2, 0.1);opacity:0;transform:rotate(-45deg)}to{opacity:1;transform:rotate(0deg)}}@keyframes mat-checkbox-indeterminate-checked-mixedmark{from{animation-timing-function:cubic-bezier(0.14, 0, 0, 1);opacity:1;transform:rotate(0deg)}to{opacity:0;transform:rotate(315deg)}}@keyframes mat-checkbox-indeterminate-unchecked-mixedmark{0%{animation-timing-function:linear;opacity:1;transform:scaleX(1)}32.8%,100%{opacity:0;transform:scaleX(0)}}.mat-checkbox-background,.mat-checkbox-frame{top:0;left:0;right:0;bottom:0;position:absolute;border-radius:2px;box-sizing:border-box;pointer-events:none}.mat-checkbox{display:inline-block;transition:background 400ms cubic-bezier(0.25, 0.8, 0.25, 1),box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1);cursor:pointer;-webkit-tap-highlight-color:transparent}._mat-animation-noopable.mat-checkbox{transition:none;animation:none}.mat-checkbox .mat-ripple-element:not(.mat-checkbox-persistent-ripple){opacity:.16}.mat-checkbox-layout{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:inherit;align-items:baseline;vertical-align:middle;display:inline-flex;white-space:nowrap}.mat-checkbox-label{-webkit-user-select:auto;-moz-user-select:auto;-ms-user-select:auto;user-select:auto}.mat-checkbox-inner-container{display:inline-block;height:16px;line-height:0;margin:auto;margin-right:8px;order:0;position:relative;vertical-align:middle;white-space:nowrap;width:16px;flex-shrink:0}[dir=rtl] .mat-checkbox-inner-container{margin-left:8px;margin-right:auto}.mat-checkbox-inner-container-no-side-margin{margin-left:0;margin-right:0}.mat-checkbox-frame{background-color:transparent;transition:border-color 90ms cubic-bezier(0, 0, 0.2, 0.1);border-width:2px;border-style:solid}._mat-animation-noopable .mat-checkbox-frame{transition:none}.cdk-high-contrast-active .mat-checkbox.cdk-keyboard-focused .mat-checkbox-frame{border-style:dotted}.mat-checkbox-background{align-items:center;display:inline-flex;justify-content:center;transition:background-color 90ms cubic-bezier(0, 0, 0.2, 0.1),opacity 90ms cubic-bezier(0, 0, 0.2, 0.1)}._mat-animation-noopable .mat-checkbox-background{transition:none}.cdk-high-contrast-active .mat-checkbox .mat-checkbox-background{background:none}.mat-checkbox-persistent-ripple{display:block;width:100%;height:100%;transform:none}.mat-checkbox-inner-container:hover .mat-checkbox-persistent-ripple{opacity:.04}.mat-checkbox.cdk-keyboard-focused .mat-checkbox-persistent-ripple{opacity:.12}.mat-checkbox-persistent-ripple,.mat-checkbox.mat-checkbox-disabled .mat-checkbox-inner-container:hover .mat-checkbox-persistent-ripple{opacity:0}@media(hover: none){.mat-checkbox-inner-container:hover .mat-checkbox-persistent-ripple{display:none}}.mat-checkbox-checkmark{top:0;left:0;right:0;bottom:0;position:absolute;width:100%}.mat-checkbox-checkmark-path{stroke-dashoffset:22.910259;stroke-dasharray:22.910259;stroke-width:2.1333333333px}.cdk-high-contrast-black-on-white .mat-checkbox-checkmark-path{stroke:#000 !important}.mat-checkbox-mixedmark{width:calc(100% - 6px);height:2px;opacity:0;transform:scaleX(0) rotate(0deg);border-radius:2px}.cdk-high-contrast-active .mat-checkbox-mixedmark{height:0;border-top:solid 2px;margin-top:2px}.mat-checkbox-label-before .mat-checkbox-inner-container{order:1;margin-left:8px;margin-right:auto}[dir=rtl] .mat-checkbox-label-before .mat-checkbox-inner-container{margin-left:auto;margin-right:8px}.mat-checkbox-checked .mat-checkbox-checkmark{opacity:1}.mat-checkbox-checked .mat-checkbox-checkmark-path{stroke-dashoffset:0}.mat-checkbox-checked .mat-checkbox-mixedmark{transform:scaleX(1) rotate(-45deg)}.mat-checkbox-indeterminate .mat-checkbox-checkmark{opacity:0;transform:rotate(45deg)}.mat-checkbox-indeterminate .mat-checkbox-checkmark-path{stroke-dashoffset:0}.mat-checkbox-indeterminate .mat-checkbox-mixedmark{opacity:1;transform:scaleX(1) rotate(0deg)}.mat-checkbox-unchecked .mat-checkbox-background{background-color:transparent}.mat-checkbox-disabled{cursor:default}.cdk-high-contrast-active .mat-checkbox-disabled{opacity:.5}.mat-checkbox-anim-unchecked-checked .mat-checkbox-background{animation:180ms linear 0ms mat-checkbox-fade-in-background}.mat-checkbox-anim-unchecked-checked .mat-checkbox-checkmark-path{animation:180ms linear 0ms mat-checkbox-unchecked-checked-checkmark-path}.mat-checkbox-anim-unchecked-indeterminate .mat-checkbox-background{animation:180ms linear 0ms mat-checkbox-fade-in-background}.mat-checkbox-anim-unchecked-indeterminate .mat-checkbox-mixedmark{animation:90ms linear 0ms mat-checkbox-unchecked-indeterminate-mixedmark}.mat-checkbox-anim-checked-unchecked .mat-checkbox-background{animation:180ms linear 0ms mat-checkbox-fade-out-background}.mat-checkbox-anim-checked-unchecked .mat-checkbox-checkmark-path{animation:90ms linear 0ms mat-checkbox-checked-unchecked-checkmark-path}.mat-checkbox-anim-checked-indeterminate .mat-checkbox-checkmark{animation:90ms linear 0ms mat-checkbox-checked-indeterminate-checkmark}.mat-checkbox-anim-checked-indeterminate .mat-checkbox-mixedmark{animation:90ms linear 0ms mat-checkbox-checked-indeterminate-mixedmark}.mat-checkbox-anim-indeterminate-checked .mat-checkbox-checkmark{animation:500ms linear 0ms mat-checkbox-indeterminate-checked-checkmark}.mat-checkbox-anim-indeterminate-checked .mat-checkbox-mixedmark{animation:500ms linear 0ms mat-checkbox-indeterminate-checked-mixedmark}.mat-checkbox-anim-indeterminate-unchecked .mat-checkbox-background{animation:180ms linear 0ms mat-checkbox-fade-out-background}.mat-checkbox-anim-indeterminate-unchecked .mat-checkbox-mixedmark{animation:300ms linear 0ms mat-checkbox-indeterminate-unchecked-mixedmark}.mat-checkbox-input{bottom:0;left:50%}.mat-checkbox .mat-checkbox-ripple{position:absolute;left:calc(50% - 20px);top:calc(50% - 20px);height:40px;width:40px;z-index:1;pointer-events:none}\n"],encapsulation:2,changeDetection:0}),e})(),Ok=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({}),e})(),Rk=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[rv,qg,Cg,Ok],qg,Ok]}),e})();function Lk(e,t){if(1&e&&(To(0,"mat-option",8),da(1),Oo()),2&e){const e=t.$implicit;Eo("value",e),Zr(1),pa(" ",e.subject," ")}}function Dk(e,t){if(1&e&&(To(0,"mat-option",8),da(1),Oo()),2&e){const e=t.$implicit;Eo("value",e),Zr(1),pa(" ",e," ")}}function Pk(e,t){if(1&e&&(To(0,"mat-option",8),da(1),Oo()),2&e){const e=t.$implicit;Eo("value",e),Zr(1),pa(" ",e.subject," ")}}let Ik=(()=>{class e{constructor(e,t){this._build=e,this._tests=t,this.repo=this._build.active_repo,this.driver=this._build.active_driver,this.driver_commit=this._build.active_commit,this.driver_commits=this._build.driver_commits,this.spec_commit=this._tests.active_commit,this.spec_commits=this._tests.commit_list,this.spec_file=this._tests.active_spec,this.specs=this._tests.spec_list,this.settings=this._tests.settings,this.setCommit=e=>this._build.setCommit(e),this.setSpecFile=e=>this._tests.setSpec(e),this.setSpecCommit=e=>this._tests.setCommit(e),this.setSettings=e=>this._tests.setSettings(e)}}return e.\u0275fac=function(t){return new(t||e)(xo(cw),xo(uw))},e.\u0275cmp=$e({type:e,selectors:[["workbench-form"]],decls:46,vars:30,consts:[[1,"py-2","flex","flex-col"],[1,"py-4"],[1,"flex","space-x-2","flex-wrap"],[1,"py-2","flex","flex-col","flex-1"],["appearance","outline"],[3,"ngModel","ngModelChange"],[3,"value",4,"ngFor","ngForOf"],[1,"py-2","flex","items-center"],[3,"value"]],template:function(e,t){1&e&&(To(0,"div",0),To(1,"label"),da(2,"Repository"),Oo(),To(3,"div",1),da(4),Nl(5,"async"),Oo(),Oo(),To(6,"div",2),To(7,"div",3),To(8,"label"),da(9,"Driver"),Oo(),To(10,"div",1),da(11),Nl(12,"async"),Oo(),Oo(),To(13,"div",3),To(14,"label"),da(15,"Commit"),Oo(),To(16,"mat-form-field",4),To(17,"mat-select",5),Ho("ngModelChange",(function(e){return t.setCommit(e)})),Nl(18,"async"),So(19,Lk,2,2,"mat-option",6),Nl(20,"async"),Oo(),Oo(),Oo(),Oo(),To(21,"div",2),To(22,"div",3),To(23,"label"),da(24,"Spec File"),Oo(),To(25,"mat-form-field",4),To(26,"mat-select",5),Ho("ngModelChange",(function(e){return t.setSpecFile(e)})),Nl(27,"async"),So(28,Dk,2,2,"mat-option",6),Nl(29,"async"),Oo(),Oo(),Oo(),To(30,"div",3),To(31,"label"),da(32,"Spec File Commit"),Oo(),To(33,"mat-form-field",4),To(34,"mat-select",5),Ho("ngModelChange",(function(e){return t.setSpecCommit(e)})),Nl(35,"async"),So(36,Pk,2,2,"mat-option",6),Nl(37,"async"),Oo(),Oo(),Oo(),Oo(),To(38,"div",7),To(39,"mat-checkbox",5),Ho("ngModelChange",(function(e){return t.setSettings({force:e})})),Nl(40,"async"),da(41,"Force Recompilation"),Oo(),Oo(),To(42,"div",7),To(43,"mat-checkbox",5),Ho("ngModelChange",(function(e){return t.setSettings({debug_symbols:e})})),Nl(44,"async"),da(45,"Compile with Debug Symbols"),Oo(),Oo()),2&e&&(Zr(4),fa(Vl(5,10,t.repo)),Zr(7),fa(Vl(12,12,t.driver)),Zr(6),Eo("ngModel",Vl(18,14,t.driver_commit)),Zr(2),Eo("ngForOf",Vl(20,16,t.driver_commits)),Zr(7),Eo("ngModel",Vl(27,18,t.spec_file)),Zr(2),Eo("ngForOf",Vl(29,20,t.specs)),Zr(6),Eo("ngModel",Vl(35,22,t.spec_commit)),Zr(2),Eo("ngForOf",Vl(37,24,t.spec_commits)),Zr(3),Eo("ngModel",Vl(40,26,t.settings).force),Zr(4),Eo("ngModel",Vl(44,28,t.settings).debug_symbols))},directives:[qw,pk,dS,NS,Lh,Tk,mv],pipes:[zh],styles:["[_nghost-%COMP%] {\n padding: 1rem;\n }\n\n label[_ngcontent-%COMP%] {\n width: 10rem;\n }\n\n mat-form-field[_ngcontent-%COMP%] {\n height: 3.5rem;\n min-width: 16rem;\n }"]}),e})(),Mk=(()=>{class e{constructor(){this._timers={},this._intervals={},this._subscriptions={},this._initialised=new Qv(!1),this.initialised=this._initialised.asObservable()}get is_initialised(){return this._initialised.getValue()}ngOnDestroy(){this.destroy()}destroy(){for(const e in this._timers)this._timers.hasOwnProperty(e)&&this.clearTimeout(e);for(const e in this._intervals)this._intervals.hasOwnProperty(e)&&this.clearInterval(e);for(const e in this._subscriptions)this._subscriptions.hasOwnProperty(e)&&this.unsub(e)}timeout(e,t,i=300){if(!(e&&t&&t instanceof Function))throw new Error(e?"Cannot create named timeout without a name":"Cannot create a timeout without a callback");this.clearTimeout(e),this._timers[e]=setTimeout(()=>{t(),this._timers[e]=null},i)}clearTimeout(e){this._timers[e]&&(clearTimeout(this._timers[e]),this._timers[e]=null)}interval(e,t,i=300){if(!(e&&t&&t instanceof Function))throw new Error(e?"Cannot create named interval without a name":"Cannot create a interval without a callback");this.clearInterval(e),this._intervals[e]=setInterval(()=>t(),i)}clearInterval(e){this._intervals[e]&&(clearInterval(this._intervals[e]),this._intervals[e]=null)}subscription(e,t){this.unsub(e),this._subscriptions[e]=t}unsub(e){this._subscriptions&&this._subscriptions[e]&&(this._subscriptions[e]instanceof u?this._subscriptions[e].unsubscribe():this._subscriptions[e](),this._subscriptions[e]=null)}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275prov=pe({token:e,factory:e.\u0275fac,providedIn:"root"}),e})();var Fk=i("/POA");const Hk=["terminal"],Bk=["container"];let jk=(()=>{class e extends Mk{ngOnInit(){this.terminal&&this.ngOnDestroy(),this.terminal=new Fk.Terminal({theme:{background:"#212121",red:"#e53935",blue:"#1e88e5",yellow:"#fdd835",green:"#43a047"},fontSize:14}),this.terminal.open(this.terminal_element.nativeElement),this.timeout("init",()=>{this.resizeTerminal(),this.updateTerminalContents(this.content||"")})}ngOnChanges(e){e.content&&this.updateTerminalContents(this.content||""),e.resize&&this.timeout("resize",()=>this.resizeTerminal())}ngOnDestroy(){this.terminal.clear(),this.terminal.dispose()}resizeTerminal(){if(!this.terminal||!this.container_el)return;const e=this.terminal.getOption("fontSize"),t=this.terminal.getOption("lineHeight"),i=this.container_el.nativeElement.getBoundingClientRect(),n=Math.floor(i.width/(.6*e)),r=Math.floor(i.height/(t*e*1.2));this.terminal.resize(n-2,r)}updateTerminalContents(e){if(!this.terminal)return;this.terminal.clear();const t=e.split("\n");for(const i of t)this.terminal.writeln(i);this.timeout("scroll",()=>this.terminal.scrollToBottom(),50)}}return e.\u0275fac=function(t){return Nk(t||e)},e.\u0275cmp=$e({type:e,selectors:[["a-terminal"]],viewQuery:function(e,t){if(1&e&&(ec(Hk,3),ec(Bk,3)),2&e){let e;Jl(e=ic())&&(t.terminal_element=e.first),Jl(e=ic())&&(t.container_el=e.first)}},inputs:{content:"content",resize:"resize"},features:[lo,dt],decls:4,vars:0,consts:[["name","terminal",1,"w-full","h-full","overflow-hidden",3,"resize"],["container",""],[1,"terminal"],["terminal",""]],template:function(e,t){1&e&&(To(0,"div",0,1),Ho("resize",(function(){return t.resizeTerminal()}),!1,lr),Ro(2,"div",2,3),Oo())},styles:['[name="terminal"][_ngcontent-%COMP%] {\n background-color: #212121;\n }\n\n .terminal[_ngcontent-%COMP%] {\n max-width: 100%;\n min-height: 100%;\n }']}),e})();const Nk=Ki(jk);function Vk(e,t){if(1&e&&(ui(),Ro(0,"circle",3)),2&e){const e=Uo();Qo("animation-name","mat-progress-spinner-stroke-rotate-"+e._spinnerAnimationLabel)("stroke-dashoffset",e._getStrokeDashOffset(),"px")("stroke-dasharray",e._getStrokeCircumference(),"px")("stroke-width",e._getCircleStrokeWidth(),"%"),Co("r",e._getCircleRadius())}}function Uk(e,t){if(1&e&&(ui(),Ro(0,"circle",3)),2&e){const e=Uo();Qo("stroke-dashoffset",e._getStrokeDashOffset(),"px")("stroke-dasharray",e._getStrokeCircumference(),"px")("stroke-width",e._getCircleStrokeWidth(),"%"),Co("r",e._getCircleRadius())}}function qk(e,t){if(1&e&&(ui(),Ro(0,"circle",3)),2&e){const e=Uo();Qo("animation-name","mat-progress-spinner-stroke-rotate-"+e._spinnerAnimationLabel)("stroke-dashoffset",e._getStrokeDashOffset(),"px")("stroke-dasharray",e._getStrokeCircumference(),"px")("stroke-width",e._getCircleStrokeWidth(),"%"),Co("r",e._getCircleRadius())}}function zk(e,t){if(1&e&&(ui(),Ro(0,"circle",3)),2&e){const e=Uo();Qo("stroke-dashoffset",e._getStrokeDashOffset(),"px")("stroke-dasharray",e._getStrokeCircumference(),"px")("stroke-width",e._getCircleStrokeWidth(),"%"),Co("r",e._getCircleRadius())}}const Wk=".mat-progress-spinner{display:block;position:relative;overflow:hidden}.mat-progress-spinner svg{position:absolute;transform:rotate(-90deg);top:0;left:0;transform-origin:center;overflow:visible}.mat-progress-spinner circle{fill:transparent;transform-origin:center;transition:stroke-dashoffset 225ms linear}._mat-animation-noopable.mat-progress-spinner circle{transition:none;animation:none}.cdk-high-contrast-active .mat-progress-spinner circle{stroke:currentColor}.mat-progress-spinner.mat-progress-spinner-indeterminate-animation[mode=indeterminate] svg{animation:mat-progress-spinner-linear-rotate 2000ms linear infinite}._mat-animation-noopable.mat-progress-spinner.mat-progress-spinner-indeterminate-animation[mode=indeterminate] svg{transition:none;animation:none}.mat-progress-spinner.mat-progress-spinner-indeterminate-animation[mode=indeterminate] circle{transition-property:stroke;animation-duration:4000ms;animation-timing-function:cubic-bezier(0.35, 0, 0.25, 1);animation-iteration-count:infinite}._mat-animation-noopable.mat-progress-spinner.mat-progress-spinner-indeterminate-animation[mode=indeterminate] circle{transition:none;animation:none}.mat-progress-spinner.mat-progress-spinner-indeterminate-fallback-animation[mode=indeterminate] svg{animation:mat-progress-spinner-stroke-rotate-fallback 10000ms cubic-bezier(0.87, 0.03, 0.33, 1) infinite}._mat-animation-noopable.mat-progress-spinner.mat-progress-spinner-indeterminate-fallback-animation[mode=indeterminate] svg{transition:none;animation:none}.mat-progress-spinner.mat-progress-spinner-indeterminate-fallback-animation[mode=indeterminate] circle{transition-property:stroke}._mat-animation-noopable.mat-progress-spinner.mat-progress-spinner-indeterminate-fallback-animation[mode=indeterminate] circle{transition:none;animation:none}@keyframes mat-progress-spinner-linear-rotate{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@keyframes mat-progress-spinner-stroke-rotate-100{0%{stroke-dashoffset:268.606171575px;transform:rotate(0)}12.5%{stroke-dashoffset:56.5486677px;transform:rotate(0)}12.5001%{stroke-dashoffset:56.5486677px;transform:rotateX(180deg) rotate(72.5deg)}25%{stroke-dashoffset:268.606171575px;transform:rotateX(180deg) rotate(72.5deg)}25.0001%{stroke-dashoffset:268.606171575px;transform:rotate(270deg)}37.5%{stroke-dashoffset:56.5486677px;transform:rotate(270deg)}37.5001%{stroke-dashoffset:56.5486677px;transform:rotateX(180deg) rotate(161.5deg)}50%{stroke-dashoffset:268.606171575px;transform:rotateX(180deg) rotate(161.5deg)}50.0001%{stroke-dashoffset:268.606171575px;transform:rotate(180deg)}62.5%{stroke-dashoffset:56.5486677px;transform:rotate(180deg)}62.5001%{stroke-dashoffset:56.5486677px;transform:rotateX(180deg) rotate(251.5deg)}75%{stroke-dashoffset:268.606171575px;transform:rotateX(180deg) rotate(251.5deg)}75.0001%{stroke-dashoffset:268.606171575px;transform:rotate(90deg)}87.5%{stroke-dashoffset:56.5486677px;transform:rotate(90deg)}87.5001%{stroke-dashoffset:56.5486677px;transform:rotateX(180deg) rotate(341.5deg)}100%{stroke-dashoffset:268.606171575px;transform:rotateX(180deg) rotate(341.5deg)}}@keyframes mat-progress-spinner-stroke-rotate-fallback{0%{transform:rotate(0deg)}25%{transform:rotate(1170deg)}50%{transform:rotate(2340deg)}75%{transform:rotate(3510deg)}100%{transform:rotate(4680deg)}}\n";class $k{constructor(e){this._elementRef=e}}const Kk=Wg($k,"primary"),Gk=new Qi("mat-progress-spinner-default-options",{providedIn:"root",factory:function(){return{diameter:100}}});let Zk=(()=>{class e extends Kk{constructor(t,i,n,r,s){super(t),this._elementRef=t,this._document=n,this._diameter=100,this._value=0,this._fallbackAnimation=!1,this.mode="determinate";const o=e._diameters;this._spinnerAnimationLabel=this._getSpinnerAnimationLabel(),o.has(n.head)||o.set(n.head,new Set([100])),this._fallbackAnimation=i.EDGE||i.TRIDENT,this._noopAnimations="NoopAnimations"===r&&!!s&&!s._forceAnimations,s&&(s.diameter&&(this.diameter=s.diameter),s.strokeWidth&&(this.strokeWidth=s.strokeWidth))}get diameter(){return this._diameter}set diameter(e){this._diameter=P_(e),this._spinnerAnimationLabel=this._getSpinnerAnimationLabel(),!this._fallbackAnimation&&this._styleRoot&&this._attachStyleNode()}get strokeWidth(){return this._strokeWidth||this.diameter/10}set strokeWidth(e){this._strokeWidth=P_(e)}get value(){return"determinate"===this.mode?this._value:0}set value(e){this._value=Math.max(0,Math.min(100,P_(e)))}ngOnInit(){const e=this._elementRef.nativeElement;this._styleRoot=fm(e)||this._document.head,this._attachStyleNode(),e.classList.add(`mat-progress-spinner-indeterminate${this._fallbackAnimation?"-fallback":""}-animation`)}_getCircleRadius(){return(this.diameter-10)/2}_getViewBox(){const e=2*this._getCircleRadius()+this.strokeWidth;return`0 0 ${e} ${e}`}_getStrokeCircumference(){return 2*Math.PI*this._getCircleRadius()}_getStrokeDashOffset(){return"determinate"===this.mode?this._getStrokeCircumference()*(100-this._value)/100:this._fallbackAnimation&&"indeterminate"===this.mode?.2*this._getStrokeCircumference():null}_getCircleStrokeWidth(){return this.strokeWidth/this.diameter*100}_attachStyleNode(){const t=this._styleRoot,i=this._diameter,n=e._diameters;let r=n.get(t);if(!r||!r.has(i)){const e=this._document.createElement("style");e.setAttribute("mat-spinner-animation",this._spinnerAnimationLabel),e.textContent=this._getAnimationText(),t.appendChild(e),r||(r=new Set,n.set(t,r)),r.add(i)}}_getAnimationText(){const e=this._getStrokeCircumference();return"\n @keyframes mat-progress-spinner-stroke-rotate-DIAMETER {\n 0% { stroke-dashoffset: START_VALUE; transform: rotate(0); }\n 12.5% { stroke-dashoffset: END_VALUE; transform: rotate(0); }\n 12.5001% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(72.5deg); }\n 25% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(72.5deg); }\n\n 25.0001% { stroke-dashoffset: START_VALUE; transform: rotate(270deg); }\n 37.5% { stroke-dashoffset: END_VALUE; transform: rotate(270deg); }\n 37.5001% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(161.5deg); }\n 50% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(161.5deg); }\n\n 50.0001% { stroke-dashoffset: START_VALUE; transform: rotate(180deg); }\n 62.5% { stroke-dashoffset: END_VALUE; transform: rotate(180deg); }\n 62.5001% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(251.5deg); }\n 75% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(251.5deg); }\n\n 75.0001% { stroke-dashoffset: START_VALUE; transform: rotate(90deg); }\n 87.5% { stroke-dashoffset: END_VALUE; transform: rotate(90deg); }\n 87.5001% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(341.5deg); }\n 100% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(341.5deg); }\n }\n".replace(/START_VALUE/g,""+.95*e).replace(/END_VALUE/g,""+.2*e).replace(/DIAMETER/g,""+this._spinnerAnimationLabel)}_getSpinnerAnimationLabel(){return this.diameter.toString().replace(".","_")}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(rm),xo(ah,8),xo(ap,8),xo(Gk))},e.\u0275cmp=$e({type:e,selectors:[["mat-progress-spinner"]],hostAttrs:["role","progressbar",1,"mat-progress-spinner"],hostVars:10,hostBindings:function(e,t){2&e&&(Co("aria-valuemin","determinate"===t.mode?0:null)("aria-valuemax","determinate"===t.mode?100:null)("aria-valuenow","determinate"===t.mode?t.value:null)("mode",t.mode),Qo("width",t.diameter,"px")("height",t.diameter,"px"),Jo("_mat-animation-noopable",t._noopAnimations))},inputs:{color:"color",mode:"mode",diameter:"diameter",strokeWidth:"strokeWidth",value:"value"},exportAs:["matProgressSpinner"],features:[lo],decls:3,vars:8,consts:[["preserveAspectRatio","xMidYMid meet","focusable","false","aria-hidden","true",3,"ngSwitch"],["cx","50%","cy","50%",3,"animation-name","stroke-dashoffset","stroke-dasharray","stroke-width",4,"ngSwitchCase"],["cx","50%","cy","50%",3,"stroke-dashoffset","stroke-dasharray","stroke-width",4,"ngSwitchCase"],["cx","50%","cy","50%"]],template:function(e,t){1&e&&(ui(),To(0,"svg",0),So(1,Vk,1,9,"circle",1),So(2,Uk,1,7,"circle",2),Oo()),2&e&&(Qo("width",t.diameter,"px")("height",t.diameter,"px"),Eo("ngSwitch","indeterminate"===t.mode),Co("viewBox",t._getViewBox()),Zr(1),Eo("ngSwitchCase",!0),Zr(1),Eo("ngSwitchCase",!1))},directives:[Hh,Bh],styles:[Wk],encapsulation:2,changeDetection:0}),e._diameters=new WeakMap,e})(),Yk=(()=>{class e extends Zk{constructor(e,t,i,n,r){super(e,t,i,n,r),this.mode="indeterminate"}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(rm),xo(ah,8),xo(ap,8),xo(Gk))},e.\u0275cmp=$e({type:e,selectors:[["mat-spinner"]],hostAttrs:["role","progressbar","mode","indeterminate",1,"mat-spinner","mat-progress-spinner"],hostVars:6,hostBindings:function(e,t){2&e&&(Qo("width",t.diameter,"px")("height",t.diameter,"px"),Jo("_mat-animation-noopable",t._noopAnimations))},inputs:{color:"color"},features:[lo],decls:3,vars:8,consts:[["preserveAspectRatio","xMidYMid meet","focusable","false","aria-hidden","true",3,"ngSwitch"],["cx","50%","cy","50%",3,"animation-name","stroke-dashoffset","stroke-dasharray","stroke-width",4,"ngSwitchCase"],["cx","50%","cy","50%",3,"stroke-dashoffset","stroke-dasharray","stroke-width",4,"ngSwitchCase"],["cx","50%","cy","50%"]],template:function(e,t){1&e&&(ui(),To(0,"svg",0),So(1,qk,1,9,"circle",1),So(2,zk,1,7,"circle",2),Oo()),2&e&&(Qo("width",t.diameter,"px")("height",t.diameter,"px"),Eo("ngSwitch","indeterminate"===t.mode),Co("viewBox",t._getViewBox()),Zr(1),Eo("ngSwitchCase",!0),Zr(1),Eo("ngSwitchCase",!1))},directives:[Hh,Bh],styles:[Wk],encapsulation:2,changeDetection:0}),e})(),Xk=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[qg,Wh],qg]}),e})();const Qk=["body"];function Jk(e,t){if(1&e&&Ro(0,"a-terminal",10),2&e){const e=Uo();Eo("content",e.results||"No test results to display")("resize",e.fullscreen)}}function ex(e,t){1&e&&(To(0,"div",11),Ro(1,"mat-spinner",12),Oo()),2&e&&(Zr(1),Eo("diameter",48))}let tx=(()=>{class e extends Mk{constructor(e,t){super(),this._build=e,this._tests=t,this.results="",this.fullscreen=!1,this.running=!1,this.runTests=()=>sw(this,void 0,void 0,(function*(){this.running=!0,this.results=this.processResults(yield this._tests.runSpec({}).catch(e=>e)),this.running=!1}))}ngOnInit(){this.subscription("driver",this._build.active_driver.subscribe(()=>this.results=""))}processResults(e){e instanceof Object&&(e=e.error);const t=e.indexOf("exited with 0")>=0;return this._build.setTestStatus(t?"passed":"failed"),this.timeout("scroll",()=>this._body_el.nativeElement.scrollTo(0,this._body_el.nativeElement.scrollHeight),10),e}}return e.\u0275fac=function(t){return new(t||e)(xo(cw),xo(uw))},e.\u0275cmp=$e({type:e,selectors:[["workbench-output"]],viewQuery:function(e,t){if(1&e&&ec(Qk,1),2&e){let e;Jl(e=ic())&&(t._body_el=e.first)}},features:[lo],decls:12,vars:5,consts:[["name","output"],[1,"flex","items-center","p-2","w-full"],["mat-button","",3,"click"],[1,"flex-1","w-0"],["mat-icon-button","",3,"click"],[1,"material-icons"],[1,"flex-1","w-full","overflow-auto"],["body",""],[3,"content","resize",4,"ngIf"],["class","absolute inset-0 bg-white bg-opacity-25 flex items-center justify-center",4,"ngIf"],[3,"content","resize"],[1,"absolute","inset-0","bg-white","bg-opacity-25","flex","items-center","justify-center"],[3,"diameter"]],template:function(e,t){1&e&&(To(0,"div",0),To(1,"div",1),To(2,"button",2),Ho("click",(function(){return t.runTests()})),da(3,"Run!"),Oo(),Ro(4,"div",3),To(5,"button",4),Ho("click",(function(){return t.fullscreen=!t.fullscreen})),To(6,"i",5),da(7),Oo(),Oo(),Oo(),To(8,"div",6,7),So(10,Jk,1,2,"a-terminal",8),Oo(),So(11,ex,2,1,"div",9),Oo()),2&e&&(ea("absolute inset-0 flex flex-col border-t border-white bg-gray-800 text-white "+(t.fullscreen?"fullscreen":"")),Zr(7),fa(t.fullscreen?"keyboard_arrow_down":"keyboard_arrow_up"),Zr(3),Eo("ngIf",!t.running),Zr(1),Eo("ngIf",t.running))},directives:[xv,Ph,jk,Yk],styles:['[_nghost-%COMP%] {\n position: relative;\n height: 100%;\n width: 100%;\n }\n\n [name="output"][_ngcontent-%COMP%] {\n transition: top 200ms;\n top: 0;\n }\n\n .fullscreen[_ngcontent-%COMP%] {\n top: -24rem;\n }']}),e})();function ix(e,t){1&e&&(Lo(0),Ro(1,"workbench-form",2),Ro(2,"workbench-output",3),Do())}function nx(e,t){1&e&&(To(0,"div",4),To(1,"i",5),da(2,"arrow_back"),Oo(),To(3,"p"),da(4,"Select a driver from the sidebar to begin"),Oo(),Oo())}let rx=(()=>{class e{constructor(e,t){this._route=e,this._build=t}ngOnInit(){this._route.paramMap.subscribe(e=>{e.has("repo")&&this._build.setRepository(e.get("repo")),e.has("driver")&&this._build.setDriver(e.get("driver")),this.driver=e.get("driver")||""})}}return e.\u0275fac=function(t){return new(t||e)(xo(bb),xo(cw))},e.\u0275cmp=$e({type:e,selectors:[["app-workbench"]],decls:3,vars:2,consts:[[4,"ngIf","ngIfElse"],["empty_state",""],[1,"w-full"],[1,"w-full","flex-1","h-0"],[1,"absolute","inset-0","flex","flex-col","items-center","justify-center"],[1,"material-icons","m-4"]],template:function(e,t){if(1&e&&(So(0,ix,3,0,"ng-container",0),So(1,nx,5,0,"ng-template",null,1,oc)),2&e){const e=ko(2);Eo("ngIf",t.driver)("ngIfElse",e)}},directives:[Ph,Ik,tx],styles:["[_nghost-%COMP%] {\n position: relative;\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n\n i[_ngcontent-%COMP%] {\n font-size: 1.5rem;\n }"]}),e})();const sx=[{path:"",component:rx},{path:":repo",component:rx},{path:":repo/:driver",component:rx},{path:"**",redirectTo:""}];let ox,ax,lx,cx,hx=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[YC.forRoot(sx,{useHash:!0})],YC]}),e})();function ux(e,t,i,n="debug",r=!1,s="Spec Runner"){if(window.debug||r){const r=["color: #E91E63","color: #3F51B5","color: default"];i?console[n](`%c[${s}]%c[${e}] %c${t}`,...r,i):console[n](`%c[${s}]%c[${e}] %c${t}`,...r)}}const dx=function(){return["/"]};let fx=(()=>{class e{constructor(e){this._build=e,this.show_sidebar=this._build.sidebar,this.toggle=()=>this._build.toggleSidebar()}}return e.\u0275fac=function(t){return new(t||e)(xo(cw))},e.\u0275cmp=$e({type:e,selectors:[["topbar-header"]],decls:9,vars:5,consts:[["mat-icon-button","",1,"text-white",3,"click"],[1,"material-icons"],[1,"h-full",3,"routerLink"],["src","assets/logo-dark.svg",1,"h-10"],[1,"px-4","text-white"],[1,"flex-1","min-w-0"]],template:function(e,t){1&e&&(To(0,"button",0),Ho("click",(function(){return t.toggle()})),To(1,"i",1),da(2),Nl(3,"async"),Oo(),Oo(),To(4,"a",2),Ro(5,"img",3),Oo(),To(6,"h2",4),da(7,"Driver Spec Runner"),Oo(),Ro(8,"div",5)),2&e&&(Zr(2),fa(Vl(3,2,t.show_sidebar)?"close":"menu"),Zr(2),Eo("routerLink",Bl(4,dx)))},directives:[xv,HC],pipes:[zh],styles:["[_nghost-%COMP%] {\n display: flex;\n align-items: center;\n padding: .5rem;\n width: 100%;\n background-color: #0A0D2E;\n }\n\n h2[_ngcontent-%COMP%] {\n margin: 0;\n }"]}),e})();const px=um({passive:!0});let _x=(()=>{class e{constructor(e,t){this._platform=e,this._ngZone=t,this._monitoredElements=new Map}monitor(e){if(!this._platform.isBrowser)return hp;const t=F_(e),i=this._monitoredElements.get(t);if(i)return i.subject;const n=new S,r="cdk-text-field-autofilled",s=e=>{"cdk-text-field-autofill-start"!==e.animationName||t.classList.contains(r)?"cdk-text-field-autofill-end"===e.animationName&&t.classList.contains(r)&&(t.classList.remove(r),this._ngZone.run(()=>n.next({target:e.target,isAutofilled:!1}))):(t.classList.add(r),this._ngZone.run(()=>n.next({target:e.target,isAutofilled:!0})))};return this._ngZone.runOutsideAngular(()=>{t.addEventListener("animationstart",s,px),t.classList.add("cdk-text-field-autofill-monitored")}),this._monitoredElements.set(t,{subject:n,unlisten:()=>{t.removeEventListener("animationstart",s,px)}}),n}stopMonitoring(e){const t=F_(e),i=this._monitoredElements.get(t);i&&(i.unlisten(),i.subject.complete(),t.classList.remove("cdk-text-field-autofill-monitored"),t.classList.remove("cdk-text-field-autofilled"),this._monitoredElements.delete(t))}ngOnDestroy(){this._monitoredElements.forEach((e,t)=>this.stopMonitoring(t))}}return e.\u0275fac=function(t){return new(t||e)(mn(rm),mn(Tc))},e.\u0275prov=pe({factory:function(){return new e(mn(rm),mn(Tc))},token:e,providedIn:"root"}),e})(),mx=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[sm]]}),e})();const gx=new Qi("MAT_INPUT_VALUE_ACCESSOR"),vx=["button","checkbox","file","hidden","image","radio","range","reset","submit"];let yx=0;class bx{constructor(e,t,i,n){this._defaultErrorStateMatcher=e,this._parentForm=t,this._parentFormGroup=i,this.ngControl=n}}const Cx=Gg(bx);let wx=(()=>{class e extends Cx{constructor(e,t,i,n,r,s,o,a,l,c){super(s,n,r,i),this._elementRef=e,this._platform=t,this.ngControl=i,this._autofillMonitor=a,this._formField=c,this._uid="mat-input-"+yx++,this.focused=!1,this.stateChanges=new S,this.controlType="mat-input",this.autofilled=!1,this._disabled=!1,this._required=!1,this._type="text",this._readonly=!1,this._neverEmptyInputTypes=["date","datetime","datetime-local","month","time","week"].filter(e=>am().has(e));const h=this._elementRef.nativeElement,u=h.nodeName.toLowerCase();this._inputValueAccessor=o||h,this._previousNativeValue=this.value,this.id=this.id,t.IOS&&l.runOutsideAngular(()=>{e.nativeElement.addEventListener("keyup",e=>{let t=e.target;t.value||t.selectionStart||t.selectionEnd||(t.setSelectionRange(1,1),t.setSelectionRange(0,0))})}),this._isServer=!this._platform.isBrowser,this._isNativeSelect="select"===u,this._isTextarea="textarea"===u,this._isNativeSelect&&(this.controlType=h.multiple?"mat-native-select-multiple":"mat-native-select")}get disabled(){return this.ngControl&&null!==this.ngControl.disabled?this.ngControl.disabled:this._disabled}set disabled(e){this._disabled=D_(e),this.focused&&(this.focused=!1,this.stateChanges.next())}get id(){return this._id}set id(e){this._id=e||this._uid}get required(){return this._required}set required(e){this._required=D_(e)}get type(){return this._type}set type(e){this._type=e||"text",this._validateType(),!this._isTextarea&&am().has(this._type)&&(this._elementRef.nativeElement.type=this._type)}get value(){return this._inputValueAccessor.value}set value(e){e!==this.value&&(this._inputValueAccessor.value=e,this.stateChanges.next())}get readonly(){return this._readonly}set readonly(e){this._readonly=D_(e)}ngAfterViewInit(){this._platform.isBrowser&&this._autofillMonitor.monitor(this._elementRef.nativeElement).subscribe(e=>{this.autofilled=e.isAutofilled,this.stateChanges.next()})}ngOnChanges(){this.stateChanges.next()}ngOnDestroy(){this.stateChanges.complete(),this._platform.isBrowser&&this._autofillMonitor.stopMonitoring(this._elementRef.nativeElement)}ngDoCheck(){this.ngControl&&this.updateErrorState(),this._dirtyCheckNativeValue(),this._dirtyCheckPlaceholder()}focus(e){this._elementRef.nativeElement.focus(e)}_focusChanged(e){e===this.focused||this.readonly&&e||(this.focused=e,this.stateChanges.next())}_onInput(){}_dirtyCheckPlaceholder(){var e,t;const i=(null===(t=null===(e=this._formField)||void 0===e?void 0:e._hideControlPlaceholder)||void 0===t?void 0:t.call(e))?null:this.placeholder;if(i!==this._previousPlaceholder){const e=this._elementRef.nativeElement;this._previousPlaceholder=i,i?e.setAttribute("placeholder",i):e.removeAttribute("placeholder")}}_dirtyCheckNativeValue(){const e=this._elementRef.nativeElement.value;this._previousNativeValue!==e&&(this._previousNativeValue=e,this.stateChanges.next())}_validateType(){vx.indexOf(this._type)}_isNeverEmpty(){return this._neverEmptyInputTypes.indexOf(this._type)>-1}_isBadInput(){let e=this._elementRef.nativeElement.validity;return e&&e.badInput}get empty(){return!(this._isNeverEmpty()||this._elementRef.nativeElement.value||this._isBadInput()||this.autofilled)}get shouldLabelFloat(){if(this._isNativeSelect){const e=this._elementRef.nativeElement,t=e.options[0];return this.focused||e.multiple||!this.empty||!!(e.selectedIndex>-1&&t&&t.label)}return this.focused||!this.empty}setDescribedByIds(e){e.length?this._elementRef.nativeElement.setAttribute("aria-describedby",e.join(" ")):this._elementRef.nativeElement.removeAttribute("aria-describedby")}onContainerClick(){this.focused||this.focus()}}return e.\u0275fac=function(t){return new(t||e)(xo(ja),xo(rm),xo(uS,10),xo(HS,8),xo(zS,8),xo(Zg),xo(gx,10),xo(_x),xo(Tc),xo(Uw,8))},e.\u0275dir=Qe({type:e,selectors:[["input","matInput",""],["textarea","matInput",""],["select","matNativeControl",""],["input","matNativeControl",""],["textarea","matNativeControl",""]],hostAttrs:[1,"mat-input-element","mat-form-field-autofill-control"],hostVars:9,hostBindings:function(e,t){1&e&&Ho("focus",(function(){return t._focusChanged(!0)}))("blur",(function(){return t._focusChanged(!1)}))("input",(function(){return t._onInput()})),2&e&&(_a("disabled",t.disabled)("required",t.required),Co("id",t.id)("data-placeholder",t.placeholder)("readonly",t.readonly&&!t._isNativeSelect||null)("aria-invalid",t.errorState&&!t.empty)("aria-required",t.required),Jo("mat-input-server",t._isServer))},inputs:{id:"id",disabled:"disabled",required:"required",type:"type",value:"value",readonly:"readonly",placeholder:"placeholder",errorStateMatcher:"errorStateMatcher",userAriaDescribedBy:["aria-describedby","userAriaDescribedBy"]},exportAs:["matInput"],features:[Da([{provide:Lw,useExisting:e}]),lo,dt]}),e})(),Sx=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[Zg],imports:[[mx,zw,qg],mx,zw]}),e})(),kx=(()=>{class e{transform(e){if(e.indexOf("/")>=0){const t=e.split("/");return t.splice(0,1),``}return e}}return e.\u0275fac=function(t){return new(t||e)},e.\u0275pipe=Je({name:"driverFormat",type:e,pure:!0}),e})();function xx(e,t){if(1&e&&(To(0,"mat-option",9),da(1),Oo()),2&e){const e=t.$implicit;Eo("value",e),Zr(1),pa(" ",e," ")}}const Ex=function(e,t){return[e,t]};function Ax(e,t){if(1&e){const e=Po();To(0,"a",11),Ho("click",(function(){Bt(e);const i=t.$implicit;return Uo(2).setDriver(i)})),To(1,"div",12),Ro(2,"div",13),Nl(3,"async"),Nl(4,"async"),Ro(5,"div",14),Nl(6,"driverFormat"),Oo(),Oo()}if(2&e){const e=t.$implicit,a=Uo(2);Eo("routerLink",(i=10,n=Ex,r="/"+a.repo,s=e,function(e,t,i,n,r,s,o){const a=t+i;return function(e,t,i,n){const r=bo(e,t,i);return bo(e,t+1,n)||r}(e,a,r,s)?yo(e,a+2,o?n.call(o,r,s):n(r,s)):jl(e,a+2)}(Ft(),$t(),i,n,r,s,o))),Zr(2),ea("mr-4 h-2 w-2 rounded-full border border-white "+(Vl(3,4,a.statues)[a.repo+"|"+e]?Vl(4,6,a.statues)[a.repo+"|"+e]:"")),Zr(3),Eo("innerHTML",Vl(6,8,e),Jn)}var i,n,r,s,o}function Tx(e,t){if(1&e&&(Lo(0),So(1,Ax,7,13,"a",10),Nl(2,"async"),Do()),2&e){const e=Uo();Zr(1),Eo("ngForOf",Vl(2,1,e.drivers))}}function Ox(e,t){1&e&&(To(0,"p",15),da(1,"No Drivers"),Oo())}let Rx=(()=>{class e{constructor(e){this._build=e,this._search_filter=new Qv(""),this.repos=this._build.repositories,this.drivers=Ov([this._build.driver_list,this._search_filter]).pipe(M(e=>{const[t,i]=e;return t.filter(e=>e.toLowerCase().includes(i.toLowerCase()))})),this.statues=this._build.test_statuses,this.setRepo=e=>this._build.setRepository(e),this.setDriver=e=>this._build.setDriver(e),this.setFilter=e=>this._search_filter.next(e),this.search_str=""}get repo(){return this._build.getRepository()}}return e.\u0275fac=function(t){return new(t||e)(xo(cw))},e.\u0275cmp=$e({type:e,selectors:[["sidebar"]],decls:13,vars:9,consts:[["appearance","outline",1,"m-2"],[3,"ngModel","ngModelChange"],[3,"value",4,"ngFor","ngForOf"],["appearance","outline",1,"mb-2","mx-2"],["matPrefix","",1,"material-icons"],["matInput","","placeholder","Filter drivers...",3,"ngModel","ngModelChange"],[1,"overflow-y-auto","flex-1","border-t","border-gray-300"],[4,"ngIf","ngIfElse"],["empty_state",""],[3,"value"],["mat-button","","class","w-full border-gray-200","routerLinkActive","active",3,"routerLink","click",4,"ngFor","ngForOf"],["mat-button","","routerLinkActive","active",1,"w-full","border-gray-200",3,"routerLink","click"],[1,"flex","items-center","my-2"],["name","dot"],[3,"innerHTML"],[1,"p-2","w-full","text-center"]],template:function(e,t){if(1&e&&(To(0,"mat-form-field",0),To(1,"mat-select",1),Ho("ngModelChange",(function(e){return t.setRepo(e)})),So(2,xx,2,2,"mat-option",2),Nl(3,"async"),Oo(),Oo(),To(4,"mat-form-field",3),To(5,"i",4),da(6,"search"),Oo(),To(7,"input",5),Ho("ngModelChange",(function(e){return t.search_str=e}))("ngModelChange",(function(e){return t.setFilter(e)})),Oo(),Oo(),To(8,"div",6),So(9,Tx,3,3,"ng-container",7),Nl(10,"async"),Oo(),So(11,Ox,2,0,"ng-template",null,8,oc)),2&e){const e=ko(12);let i=null;Zr(1),Eo("ngModel",t.repo),Zr(1),Eo("ngForOf",Vl(3,5,t.repos)),Zr(5),Eo("ngModel",t.search_str),Zr(2),Eo("ngIf",null==(i=Vl(10,7,t.drivers))?null:i.length)("ngIfElse",e)}},directives:[qw,pk,dS,NS,Lh,Fw,wx,Zw,Ph,mv,Ev,HC,jC],pipes:[zh,kx],styles:['[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n max-width: 20rem;\n }\n\n mat-form-field[_ngcontent-%COMP%] {\n height: 3.5rem;\n }\n\n p[_ngcontent-%COMP%] {\n margin: 1rem 0;\n }\n\n [name="dot"][_ngcontent-%COMP%] {\n background-color: #ffb300;\n }\n\n [name="dot"].failed[_ngcontent-%COMP%] {\n background-color: #d50000;\n }\n\n [name="dot"].passed[_ngcontent-%COMP%] {\n background-color: #43a047;\n }\n\n a[_ngcontent-%COMP%] {\n border-bottom: 1px solid #edf2f7;\n border-radius: 0;\n }\n\n a.active[_ngcontent-%COMP%] {\n background-color: #c92366;\n color: #fff;\n }']}),e})(),Lx=(()=>{class e{constructor(e,t,i){this._snackbar=e,this._cache=t,this._build=i,this.show_sidebar=this._build.sidebar}ngOnInit(){ox=this._snackbar,function(e,t=(()=>null),i=3e5){e.isEnabled&&(ax&&ax.unsubscribe(),lx&&lx.unsubscribe(),cx&&clearInterval(cx),ax=e.available.subscribe(e=>{ux("CACHE",`Update available: ${"current version is "+e.current.hash} ${"available version is "+e.available.hash}`),function(){this._cache.isEnabled&&(ux("CACHE","Activating changes to the cache..."),this._cache.activateUpdate().then(()=>{var e,t;e="Newer version of the application is available",t=()=>location.reload(!0),console.info(e),function(e,t,i="OK",n,r={type:"icon",class:"material-icons",content:"info"}){if(!ox)throw new Error("Snackbar service hasn't been initialised");const s=ox.open(t,i,{panelClass:[e],duration:5e3});i&&(n=n||(()=>s.dismiss()),s.onAction().subscribe(()=>n()))}("info",e,"Refresh",t)}))}()}),lx=e.activated.subscribe(()=>{ux("CACHE","Updates activated. Reloading..."),t("Newer version of the application is available",()=>location.reload(!0))}),cx=setInterval(()=>{ux("CACHE","Checking for updates..."),this._cache.checkForUpdate()},i))}(this._cache)}}return e.\u0275fac=function(t){return new(t||e)(xo(Xv),xo(Wp),xo(cw))},e.\u0275cmp=$e({type:e,selectors:[["app-root"]],decls:7,vars:4,consts:[[1,"absolute","inset-0","overflow-hidden","flex","flex-col"],[1,"z-20"],[1,"flex","flex-1","w-full",2,"height","50%"],[1,"h-full","shadow","z-10","overflow-hidden"],["name","content",1,"h-full","flex-1","w-1/2","bg-gray-200","z-0"]],template:function(e,t){1&e&&(To(0,"div",0),Ro(1,"topbar-header",1),To(2,"div",2),To(3,"sidebar",3),Nl(4,"async"),Oo(),To(5,"div",4),Ro(6,"router-outlet"),Oo(),Oo(),Oo()),2&e&&(Zr(3),Jo("show",Vl(4,2,t.show_sidebar)))},directives:[fx,Rx,NC],pipes:[zh],styles:[".formatted-driver-name,.formatted-driver-name .icon{display:flex;align-items:center}.formatted-driver-name .icon{justify-content:center;height:1.2em;width:1.2em;font-size:1.5em}.xterm-helper-textarea{opacity:0}",".mat-form-field .mat-select-value,.mat-form-field input{position:relative;top:-4px}.dark-mode a[button],.dark-mode button.mat-button,a[button],button.mat-button{background-color:#c92366;border:1px solid #c92366;color:#fff;min-height:3em}.dark-mode a[button].inverse,.dark-mode button.mat-button.inverse,a[button].inverse,button.mat-button.inverse{background-color:#fff;color:#c92366}.dark-mode a[button].success,.dark-mode button.mat-button.success,a[button].success,button.mat-button.success{background-color:#43a047;border-color:#43a047}.dark-mode a[button].clear,.dark-mode button.mat-button.clear,a[button].clear,button.mat-button.clear{background:none;border:none;color:rgba(0,0,0,.85)}.dark-mode a[button].error,.dark-mode button.mat-button.error,a[button].error,button.mat-button.error{background:none;color:#e53935;border-color:#e53935}.dark-mode a[button][disabled],.dark-mode button.mat-button[disabled],a[button][disabled],button.mat-button[disabled]{background-color:#ccc;border-color:rgba(0,0,0,.1);pointer-events:none}.mat-spinner circle{stroke:#c92366}.xterm{padding:1rem}sidebar{width:0;transition:width .2s}sidebar.show{width:20rem}","label{font-size:.75rem;font-weight:600}"],encapsulation:2}),e})();const Dx={provide:new Qi("mat-autocomplete-scroll-strategy"),deps:[ag],useFactory:function(e){return()=>e.scrollStrategies.reposition()}};let Px=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({providers:[Dx],imports:[[fg,vv,qg,Wh],bm,vv,qg]}),e})();const Ix=[zw,Sx,Px,Av,mk,Rk,Xk],Mx=[$S,KS];let Fx=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e}),e.\u0275inj=_e({imports:[[Wh,YC,...Ix,...Mx],zw,Sx,Px,Av,mk,Rk,Xk,$S,KS]}),e})(),Hx=(()=>{class e{}return e.\u0275fac=function(t){return new(t||e)},e.\u0275mod=Ye({type:e,bootstrap:[Lx]}),e.\u0275inj=_e({providers:[],imports:[[Su,hx,cp,Zv,L_,Fx,Qp.register("ngsw-worker.js",{enabled:!0})]]}),e})();(function(){if(Vc)throw new Error("Cannot enable prod mode after platform setup.");Nc=!1})(),Cu().bootstrapModule(Hx).catch(e=>console.error(e))},zn8P:function(e,t){function i(e){return Promise.resolve().then((function(){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}i.keys=function(){return[]},i.resolve=i,e.exports=i,i.id="zn8P"}},[[0,0]]]);
\ No newline at end of file
diff --git a/www/manifest.webmanifest b/www/manifest.webmanifest
new file mode 100644
index 00000000000..f1a06c80b03
--- /dev/null
+++ b/www/manifest.webmanifest
@@ -0,0 +1,59 @@
+{
+ "name": "driver-spec-runner",
+ "short_name": "driver-spec-runner",
+ "theme_color": "#1976d2",
+ "background_color": "#fafafa",
+ "display": "standalone",
+ "scope": "./",
+ "start_url": "./",
+ "icons": [
+ {
+ "src": "assets/icons/icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-128x128.png",
+ "sizes": "128x128",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-152x152.png",
+ "sizes": "152x152",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-384x384.png",
+ "sizes": "384x384",
+ "type": "image/png",
+ "purpose": "maskable any"
+ },
+ {
+ "src": "assets/icons/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable any"
+ }
+ ]
+}
diff --git a/www/ngsw-worker.js b/www/ngsw-worker.js
new file mode 100644
index 00000000000..3026ff6c83e
--- /dev/null
+++ b/www/ngsw-worker.js
@@ -0,0 +1,2861 @@
+(function () {
+ 'use strict';
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ /**
+ * Adapts the service worker to its runtime environment.
+ *
+ * Mostly, this is used to mock out identifiers which are otherwise read
+ * from the global scope.
+ */
+ class Adapter {
+ constructor(scopeUrl) {
+ this.scopeUrl = scopeUrl;
+ const parsedScopeUrl = this.parseUrl(this.scopeUrl);
+ // Determine the origin from the registration scope. This is used to differentiate between
+ // relative and absolute URLs.
+ this.origin = parsedScopeUrl.origin;
+ // Suffixing `ngsw` with the baseHref to avoid clash of cache names for SWs with different
+ // scopes on the same domain.
+ this.cacheNamePrefix = 'ngsw:' + parsedScopeUrl.path;
+ }
+ /**
+ * Wrapper around the `Request` constructor.
+ */
+ newRequest(input, init) {
+ return new Request(input, init);
+ }
+ /**
+ * Wrapper around the `Response` constructor.
+ */
+ newResponse(body, init) {
+ return new Response(body, init);
+ }
+ /**
+ * Wrapper around the `Headers` constructor.
+ */
+ newHeaders(headers) {
+ return new Headers(headers);
+ }
+ /**
+ * Test if a given object is an instance of `Client`.
+ */
+ isClient(source) {
+ return (source instanceof Client);
+ }
+ /**
+ * Read the current UNIX time in milliseconds.
+ */
+ get time() {
+ return Date.now();
+ }
+ /**
+ * Get a normalized representation of a URL such as those found in the ServiceWorker's `ngsw.json`
+ * configuration.
+ *
+ * More specifically:
+ * 1. Resolve the URL relative to the ServiceWorker's scope.
+ * 2. If the URL is relative to the ServiceWorker's own origin, then only return the path part.
+ * Otherwise, return the full URL.
+ *
+ * @param url The raw request URL.
+ * @return A normalized representation of the URL.
+ */
+ normalizeUrl(url) {
+ // Check the URL's origin against the ServiceWorker's.
+ const parsed = this.parseUrl(url, this.scopeUrl);
+ return (parsed.origin === this.origin ? parsed.path : url);
+ }
+ /**
+ * Parse a URL into its different parts, such as `origin`, `path` and `search`.
+ */
+ parseUrl(url, relativeTo) {
+ // Workaround a Safari bug, see
+ // https://github.com/angular/angular/issues/31061#issuecomment-503637978
+ const parsed = !relativeTo ? new URL(url) : new URL(url, relativeTo);
+ return { origin: parsed.origin, path: parsed.pathname, search: parsed.search };
+ }
+ /**
+ * Wait for a given amount of time before completing a Promise.
+ */
+ timeout(ms) {
+ return new Promise(resolve => {
+ setTimeout(() => resolve(), ms);
+ });
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ /**
+ * An error returned in rejected promises if the given key is not found in the table.
+ */
+ class NotFound {
+ constructor(table, key) {
+ this.table = table;
+ this.key = key;
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ /**
+ * An implementation of a `Database` that uses the `CacheStorage` API to serialize
+ * state within mock `Response` objects.
+ */
+ class CacheDatabase {
+ constructor(scope, adapter) {
+ this.scope = scope;
+ this.adapter = adapter;
+ this.tables = new Map();
+ }
+ 'delete'(name) {
+ if (this.tables.has(name)) {
+ this.tables.delete(name);
+ }
+ return this.scope.caches.delete(`${this.adapter.cacheNamePrefix}:db:${name}`);
+ }
+ list() {
+ return this.scope.caches.keys().then(keys => keys.filter(key => key.startsWith(`${this.adapter.cacheNamePrefix}:db:`)));
+ }
+ open(name, cacheQueryOptions) {
+ if (!this.tables.has(name)) {
+ const table = this.scope.caches.open(`${this.adapter.cacheNamePrefix}:db:${name}`)
+ .then(cache => new CacheTable(name, cache, this.adapter, cacheQueryOptions));
+ this.tables.set(name, table);
+ }
+ return this.tables.get(name);
+ }
+ }
+ /**
+ * A `Table` backed by a `Cache`.
+ */
+ class CacheTable {
+ constructor(table, cache, adapter, cacheQueryOptions) {
+ this.table = table;
+ this.cache = cache;
+ this.adapter = adapter;
+ this.cacheQueryOptions = cacheQueryOptions;
+ }
+ request(key) {
+ return this.adapter.newRequest('/' + key);
+ }
+ 'delete'(key) {
+ return this.cache.delete(this.request(key), this.cacheQueryOptions);
+ }
+ keys() {
+ return this.cache.keys().then(requests => requests.map(req => req.url.substr(1)));
+ }
+ read(key) {
+ return this.cache.match(this.request(key), this.cacheQueryOptions).then(res => {
+ if (res === undefined) {
+ return Promise.reject(new NotFound(this.table, key));
+ }
+ return res.json();
+ });
+ }
+ write(key, value) {
+ return this.cache.put(this.request(key), this.adapter.newResponse(JSON.stringify(value)));
+ }
+ }
+
+ /*! *****************************************************************************
+ Copyright (c) Microsoft Corporation.
+
+ Permission to use, copy, modify, and/or distribute this software for any
+ purpose with or without fee is hereby granted.
+
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ PERFORMANCE OF THIS SOFTWARE.
+ ***************************************************************************** */
+ function __awaiter(thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try {
+ step(generator.next(value));
+ }
+ catch (e) {
+ reject(e);
+ } }
+ function rejected(value) { try {
+ step(generator["throw"](value));
+ }
+ catch (e) {
+ reject(e);
+ } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ var UpdateCacheStatus = /*@__PURE__*/ (function (UpdateCacheStatus) {
+ UpdateCacheStatus[UpdateCacheStatus["NOT_CACHED"] = 0] = "NOT_CACHED";
+ UpdateCacheStatus[UpdateCacheStatus["CACHED_BUT_UNUSED"] = 1] = "CACHED_BUT_UNUSED";
+ UpdateCacheStatus[UpdateCacheStatus["CACHED"] = 2] = "CACHED";
+ return UpdateCacheStatus;
+ })({});
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ class SwCriticalError extends Error {
+ constructor() {
+ super(...arguments);
+ this.isCritical = true;
+ }
+ }
+ function errorToString(error) {
+ if (error instanceof Error) {
+ return `${error.message}\n${error.stack}`;
+ }
+ else {
+ return `${error}`;
+ }
+ }
+ class SwUnrecoverableStateError extends SwCriticalError {
+ constructor() {
+ super(...arguments);
+ this.isUnrecoverableState = true;
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ /**
+ * Compute the SHA1 of the given string
+ *
+ * see https://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
+ *
+ * WARNING: this function has not been designed not tested with security in mind.
+ * DO NOT USE IT IN A SECURITY SENSITIVE CONTEXT.
+ *
+ * Borrowed from @angular/compiler/src/i18n/digest.ts
+ */
+ function sha1(str) {
+ const utf8 = str;
+ const words32 = stringToWords32(utf8, Endian.Big);
+ return _sha1(words32, utf8.length * 8);
+ }
+ function sha1Binary(buffer) {
+ const words32 = arrayBufferToWords32(buffer, Endian.Big);
+ return _sha1(words32, buffer.byteLength * 8);
+ }
+ function _sha1(words32, len) {
+ const w = [];
+ let [a, b, c, d, e] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
+ words32[len >> 5] |= 0x80 << (24 - len % 32);
+ words32[((len + 64 >> 9) << 4) + 15] = len;
+ for (let i = 0; i < words32.length; i += 16) {
+ const [h0, h1, h2, h3, h4] = [a, b, c, d, e];
+ for (let j = 0; j < 80; j++) {
+ if (j < 16) {
+ w[j] = words32[i + j];
+ }
+ else {
+ w[j] = rol32(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
+ }
+ const [f, k] = fk(j, b, c, d);
+ const temp = [rol32(a, 5), f, e, k, w[j]].reduce(add32);
+ [e, d, c, b, a] = [d, c, rol32(b, 30), a, temp];
+ }
+ [a, b, c, d, e] = [add32(a, h0), add32(b, h1), add32(c, h2), add32(d, h3), add32(e, h4)];
+ }
+ return byteStringToHexString(words32ToByteString([a, b, c, d, e]));
+ }
+ function add32(a, b) {
+ return add32to64(a, b)[1];
+ }
+ function add32to64(a, b) {
+ const low = (a & 0xffff) + (b & 0xffff);
+ const high = (a >>> 16) + (b >>> 16) + (low >>> 16);
+ return [high >>> 16, (high << 16) | (low & 0xffff)];
+ }
+ // Rotate a 32b number left `count` position
+ function rol32(a, count) {
+ return (a << count) | (a >>> (32 - count));
+ }
+ var Endian = /*@__PURE__*/ (function (Endian) {
+ Endian[Endian["Little"] = 0] = "Little";
+ Endian[Endian["Big"] = 1] = "Big";
+ return Endian;
+ })({});
+ function fk(index, b, c, d) {
+ if (index < 20) {
+ return [(b & c) | (~b & d), 0x5a827999];
+ }
+ if (index < 40) {
+ return [b ^ c ^ d, 0x6ed9eba1];
+ }
+ if (index < 60) {
+ return [(b & c) | (b & d) | (c & d), 0x8f1bbcdc];
+ }
+ return [b ^ c ^ d, 0xca62c1d6];
+ }
+ function stringToWords32(str, endian) {
+ const size = (str.length + 3) >>> 2;
+ const words32 = [];
+ for (let i = 0; i < size; i++) {
+ words32[i] = wordAt(str, i * 4, endian);
+ }
+ return words32;
+ }
+ function arrayBufferToWords32(buffer, endian) {
+ const size = (buffer.byteLength + 3) >>> 2;
+ const words32 = [];
+ const view = new Uint8Array(buffer);
+ for (let i = 0; i < size; i++) {
+ words32[i] = wordAt(view, i * 4, endian);
+ }
+ return words32;
+ }
+ function byteAt(str, index) {
+ if (typeof str === 'string') {
+ return index >= str.length ? 0 : str.charCodeAt(index) & 0xff;
+ }
+ else {
+ return index >= str.byteLength ? 0 : str[index] & 0xff;
+ }
+ }
+ function wordAt(str, index, endian) {
+ let word = 0;
+ if (endian === Endian.Big) {
+ for (let i = 0; i < 4; i++) {
+ word += byteAt(str, index + i) << (24 - 8 * i);
+ }
+ }
+ else {
+ for (let i = 0; i < 4; i++) {
+ word += byteAt(str, index + i) << 8 * i;
+ }
+ }
+ return word;
+ }
+ function words32ToByteString(words32) {
+ return words32.reduce((str, word) => str + word32ToByteString(word), '');
+ }
+ function word32ToByteString(word) {
+ let str = '';
+ for (let i = 0; i < 4; i++) {
+ str += String.fromCharCode((word >>> 8 * (3 - i)) & 0xff);
+ }
+ return str;
+ }
+ function byteStringToHexString(str) {
+ let hex = '';
+ for (let i = 0; i < str.length; i++) {
+ const b = byteAt(str, i);
+ hex += (b >>> 4).toString(16) + (b & 0x0f).toString(16);
+ }
+ return hex.toLowerCase();
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ /**
+ * A group of assets that are cached in a `Cache` and managed by a given policy.
+ *
+ * Concrete classes derive from this base and specify the exact caching policy.
+ */
+ class AssetGroup {
+ constructor(scope, adapter, idle, config, hashes, db, prefix) {
+ this.scope = scope;
+ this.adapter = adapter;
+ this.idle = idle;
+ this.config = config;
+ this.hashes = hashes;
+ this.db = db;
+ this.prefix = prefix;
+ /**
+ * A deduplication cache, to make sure the SW never makes two network requests
+ * for the same resource at once. Managed by `fetchAndCacheOnce`.
+ */
+ this.inFlightRequests = new Map();
+ /**
+ * Normalized resource URLs.
+ */
+ this.urls = [];
+ /**
+ * Regular expression patterns.
+ */
+ this.patterns = [];
+ this.name = config.name;
+ // Normalize the config's URLs to take the ServiceWorker's scope into account.
+ this.urls = config.urls.map(url => adapter.normalizeUrl(url));
+ // Patterns in the config are regular expressions disguised as strings. Breathe life into them.
+ this.patterns = config.patterns.map(pattern => new RegExp(pattern));
+ // This is the primary cache, which holds all of the cached requests for this group. If a
+ // resource
+ // isn't in this cache, it hasn't been fetched yet.
+ this.cache = scope.caches.open(`${this.prefix}:${config.name}:cache`);
+ // This is the metadata table, which holds specific information for each cached URL, such as
+ // the timestamp of when it was added to the cache.
+ this.metadata = this.db.open(`${this.prefix}:${config.name}:meta`, config.cacheQueryOptions);
+ }
+ cacheStatus(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const cache = yield this.cache;
+ const meta = yield this.metadata;
+ const req = this.adapter.newRequest(url);
+ const res = yield cache.match(req, this.config.cacheQueryOptions);
+ if (res === undefined) {
+ return UpdateCacheStatus.NOT_CACHED;
+ }
+ try {
+ const data = yield meta.read(req.url);
+ if (!data.used) {
+ return UpdateCacheStatus.CACHED_BUT_UNUSED;
+ }
+ }
+ catch (_) {
+ // Error on the side of safety and assume cached.
+ }
+ return UpdateCacheStatus.CACHED;
+ });
+ }
+ /**
+ * Clean up all the cached data for this group.
+ */
+ cleanup() {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.scope.caches.delete(`${this.prefix}:${this.config.name}:cache`);
+ yield this.db.delete(`${this.prefix}:${this.config.name}:meta`);
+ });
+ }
+ /**
+ * Process a request for a given resource and return it, or return null if it's not available.
+ */
+ handleFetch(req, ctx) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const url = this.adapter.normalizeUrl(req.url);
+ // Either the request matches one of the known resource URLs, one of the patterns for
+ // dynamically matched URLs, or neither. Determine which is the case for this request in
+ // order to decide how to handle it.
+ if (this.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url))) {
+ // This URL matches a known resource. Either it's been cached already or it's missing, in
+ // which case it needs to be loaded from the network.
+ // Open the cache to check whether this resource is present.
+ const cache = yield this.cache;
+ // Look for a cached response. If one exists, it can be used to resolve the fetch
+ // operation.
+ const cachedResponse = yield cache.match(req, this.config.cacheQueryOptions);
+ if (cachedResponse !== undefined) {
+ // A response has already been cached (which presumably matches the hash for this
+ // resource). Check whether it's safe to serve this resource from cache.
+ if (this.hashes.has(url)) {
+ // This resource has a hash, and thus is versioned by the manifest. It's safe to return
+ // the response.
+ return cachedResponse;
+ }
+ else {
+ // This resource has no hash, and yet exists in the cache. Check how old this request is
+ // to make sure it's still usable.
+ if (yield this.needToRevalidate(req, cachedResponse)) {
+ this.idle.schedule(`revalidate(${this.prefix}, ${this.config.name}): ${req.url}`, () => __awaiter(this, void 0, void 0, function* () {
+ yield this.fetchAndCacheOnce(req);
+ }));
+ }
+ // In either case (revalidation or not), the cached response must be good.
+ return cachedResponse;
+ }
+ }
+ // No already-cached response exists, so attempt a fetch/cache operation. The original request
+ // may specify things like credential inclusion, but for assets these are not honored in order
+ // to avoid issues with opaque responses. The SW requests the data itself.
+ const res = yield this.fetchAndCacheOnce(this.adapter.newRequest(req.url));
+ // If this is successful, the response needs to be cloned as it might be used to respond to
+ // multiple fetch operations at the same time.
+ return res.clone();
+ }
+ else {
+ return null;
+ }
+ });
+ }
+ /**
+ * Some resources are cached without a hash, meaning that their expiration is controlled
+ * by HTTP caching headers. Check whether the given request/response pair is still valid
+ * per the caching headers.
+ */
+ needToRevalidate(req, res) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Three different strategies apply here:
+ // 1) The request has a Cache-Control header, and thus expiration needs to be based on its age.
+ // 2) The request has an Expires header, and expiration is based on the current timestamp.
+ // 3) The request has no applicable caching headers, and must be revalidated.
+ if (res.headers.has('Cache-Control')) {
+ // Figure out if there is a max-age directive in the Cache-Control header.
+ const cacheControl = res.headers.get('Cache-Control');
+ const cacheDirectives = cacheControl
+ // Directives are comma-separated within the Cache-Control header value.
+ .split(',')
+ // Make sure each directive doesn't have extraneous whitespace.
+ .map(v => v.trim())
+ // Some directives have values (like maxage and s-maxage)
+ .map(v => v.split('='));
+ // Lowercase all the directive names.
+ cacheDirectives.forEach(v => v[0] = v[0].toLowerCase());
+ // Find the max-age directive, if one exists.
+ const maxAgeDirective = cacheDirectives.find(v => v[0] === 'max-age');
+ const cacheAge = maxAgeDirective ? maxAgeDirective[1] : undefined;
+ if (!cacheAge) {
+ // No usable TTL defined. Must assume that the response is stale.
+ return true;
+ }
+ try {
+ const maxAge = 1000 * parseInt(cacheAge);
+ // Determine the origin time of this request. If the SW has metadata on the request (which
+ // it
+ // should), it will have the time the request was added to the cache. If it doesn't for some
+ // reason, the request may have a Date header which will serve the same purpose.
+ let ts;
+ try {
+ // Check the metadata table. If a timestamp is there, use it.
+ const metaTable = yield this.metadata;
+ ts = (yield metaTable.read(req.url)).ts;
+ }
+ catch (_a) {
+ // Otherwise, look for a Date header.
+ const date = res.headers.get('Date');
+ if (date === null) {
+ // Unable to determine when this response was created. Assume that it's stale, and
+ // revalidate it.
+ return true;
+ }
+ ts = Date.parse(date);
+ }
+ const age = this.adapter.time - ts;
+ return age < 0 || age > maxAge;
+ }
+ catch (_b) {
+ // Assume stale.
+ return true;
+ }
+ }
+ else if (res.headers.has('Expires')) {
+ // Determine if the expiration time has passed.
+ const expiresStr = res.headers.get('Expires');
+ try {
+ // The request needs to be revalidated if the current time is later than the expiration
+ // time, if it parses correctly.
+ return this.adapter.time > Date.parse(expiresStr);
+ }
+ catch (_c) {
+ // The expiration date failed to parse, so revalidate as a precaution.
+ return true;
+ }
+ }
+ else {
+ // No way to evaluate staleness, so assume the response is already stale.
+ return true;
+ }
+ });
+ }
+ /**
+ * Fetch the complete state of a cached resource, or return null if it's not found.
+ */
+ fetchFromCacheOnly(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const cache = yield this.cache;
+ const metaTable = yield this.metadata;
+ // Lookup the response in the cache.
+ const request = this.adapter.newRequest(url);
+ const response = yield cache.match(request, this.config.cacheQueryOptions);
+ if (response === undefined) {
+ // It's not found, return null.
+ return null;
+ }
+ // Next, lookup the cached metadata.
+ let metadata = undefined;
+ try {
+ metadata = yield metaTable.read(request.url);
+ }
+ catch (_a) {
+ // Do nothing, not found. This shouldn't happen, but it can be handled.
+ }
+ // Return both the response and any available metadata.
+ return { response, metadata };
+ });
+ }
+ /**
+ * Lookup all resources currently stored in the cache which have no associated hash.
+ */
+ unhashedResources() {
+ return __awaiter(this, void 0, void 0, function* () {
+ const cache = yield this.cache;
+ // Start with the set of all cached requests.
+ return (yield cache.keys())
+ // Normalize their URLs.
+ .map(request => this.adapter.normalizeUrl(request.url))
+ // Exclude the URLs which have hashes.
+ .filter(url => !this.hashes.has(url));
+ });
+ }
+ /**
+ * Fetch the given resource from the network, and cache it if able.
+ */
+ fetchAndCacheOnce(req, used = true) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // The `inFlightRequests` map holds information about which caching operations are currently
+ // underway for known resources. If this request appears there, another "thread" is already
+ // in the process of caching it, and this work should not be duplicated.
+ if (this.inFlightRequests.has(req.url)) {
+ // There is a caching operation already in progress for this request. Wait for it to
+ // complete, and hopefully it will have yielded a useful response.
+ return this.inFlightRequests.get(req.url);
+ }
+ // No other caching operation is being attempted for this resource, so it will be owned here.
+ // Go to the network and get the correct version.
+ const fetchOp = this.fetchFromNetwork(req);
+ // Save this operation in `inFlightRequests` so any other "thread" attempting to cache it
+ // will block on this chain instead of duplicating effort.
+ this.inFlightRequests.set(req.url, fetchOp);
+ // Make sure this attempt is cleaned up properly on failure.
+ try {
+ // Wait for a response. If this fails, the request will remain in `inFlightRequests`
+ // indefinitely.
+ const res = yield fetchOp;
+ // It's very important that only successful responses are cached. Unsuccessful responses
+ // should never be cached as this can completely break applications.
+ if (!res.ok) {
+ throw new Error(`Response not Ok (fetchAndCacheOnce): request for ${req.url} returned response ${res.status} ${res.statusText}`);
+ }
+ try {
+ // This response is safe to cache (as long as it's cloned). Wait until the cache operation
+ // is complete.
+ const cache = yield this.scope.caches.open(`${this.prefix}:${this.config.name}:cache`);
+ yield cache.put(req, res.clone());
+ // If the request is not hashed, update its metadata, especially the timestamp. This is
+ // needed for future determination of whether this cached response is stale or not.
+ if (!this.hashes.has(this.adapter.normalizeUrl(req.url))) {
+ // Metadata is tracked for requests that are unhashed.
+ const meta = { ts: this.adapter.time, used };
+ const metaTable = yield this.metadata;
+ yield metaTable.write(req.url, meta);
+ }
+ return res;
+ }
+ catch (err) {
+ // Among other cases, this can happen when the user clears all data through the DevTools,
+ // but the SW is still running and serving another tab. In that case, trying to write to the
+ // caches throws an `Entry was not found` error.
+ // If this happens the SW can no longer work correctly. This situation is unrecoverable.
+ throw new SwCriticalError(`Failed to update the caches for request to '${req.url}' (fetchAndCacheOnce): ${errorToString(err)}`);
+ }
+ }
+ finally {
+ // Finally, it can be removed from `inFlightRequests`. This might result in a double-remove
+ // if some other chain was already making this request too, but that won't hurt anything.
+ this.inFlightRequests.delete(req.url);
+ }
+ });
+ }
+ fetchFromNetwork(req, redirectLimit = 3) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Make a cache-busted request for the resource.
+ const res = yield this.cacheBustedFetchFromNetwork(req);
+ // Check for redirected responses, and follow the redirects.
+ if (res['redirected'] && !!res.url) {
+ // If the redirect limit is exhausted, fail with an error.
+ if (redirectLimit === 0) {
+ throw new SwCriticalError(`Response hit redirect limit (fetchFromNetwork): request redirected too many times, next is ${res.url}`);
+ }
+ // Unwrap the redirect directly.
+ return this.fetchFromNetwork(this.adapter.newRequest(res.url), redirectLimit - 1);
+ }
+ return res;
+ });
+ }
+ /**
+ * Load a particular asset from the network, accounting for hash validation.
+ */
+ cacheBustedFetchFromNetwork(req) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const url = this.adapter.normalizeUrl(req.url);
+ // If a hash is available for this resource, then compare the fetched version with the
+ // canonical hash. Otherwise, the network version will have to be trusted.
+ if (this.hashes.has(url)) {
+ // It turns out this resource does have a hash. Look it up. Unless the fetched version
+ // matches this hash, it's invalid and the whole manifest may need to be thrown out.
+ const canonicalHash = this.hashes.get(url);
+ // Ideally, the resource would be requested with cache-busting to guarantee the SW gets
+ // the freshest version. However, doing this would eliminate any chance of the response
+ // being in the HTTP cache. Given that the browser has recently actively loaded the page,
+ // it's likely that many of the responses the SW needs to cache are in the HTTP cache and
+ // are fresh enough to use. In the future, this could be done by setting cacheMode to
+ // *only* check the browser cache for a cached version of the resource, when cacheMode is
+ // fully supported. For now, the resource is fetched directly, without cache-busting, and
+ // if the hash test fails a cache-busted request is tried before concluding that the
+ // resource isn't correct. This gives the benefit of acceleration via the HTTP cache
+ // without the risk of stale data, at the expense of a duplicate request in the event of
+ // a stale response.
+ // Fetch the resource from the network (possibly hitting the HTTP cache).
+ let response = yield this.safeFetch(req);
+ // Decide whether a cache-busted request is necessary. A cache-busted request is necessary
+ // only if the request was successful but the hash of the retrieved contents does not match
+ // the canonical hash from the manifest.
+ let makeCacheBustedRequest = response.ok;
+ if (makeCacheBustedRequest) {
+ // The request was successful. A cache-busted request is only necessary if the hashes
+ // don't match.
+ // (Make sure to clone the response so it can be used later if it proves to be valid.)
+ const fetchedHash = sha1Binary(yield response.clone().arrayBuffer());
+ makeCacheBustedRequest = (fetchedHash !== canonicalHash);
+ }
+ // Make a cache busted request to the network, if necessary.
+ if (makeCacheBustedRequest) {
+ // Hash failure, the version that was retrieved under the default URL did not have the
+ // hash expected. This could be because the HTTP cache got in the way and returned stale
+ // data, or because the version on the server really doesn't match. A cache-busting
+ // request will differentiate these two situations.
+ // TODO: handle case where the URL has parameters already (unlikely for assets).
+ const cacheBustReq = this.adapter.newRequest(this.cacheBust(req.url));
+ response = yield this.safeFetch(cacheBustReq);
+ // If the response was successful, check the contents against the canonical hash.
+ if (response.ok) {
+ // Hash the contents.
+ // (Make sure to clone the response so it can be used later if it proves to be valid.)
+ const cacheBustedHash = sha1Binary(yield response.clone().arrayBuffer());
+ // If the cache-busted version doesn't match, then the manifest is not an accurate
+ // representation of the server's current set of files, and the SW should give up.
+ if (canonicalHash !== cacheBustedHash) {
+ throw new SwCriticalError(`Hash mismatch (cacheBustedFetchFromNetwork): ${req.url}: expected ${canonicalHash}, got ${cacheBustedHash} (after cache busting)`);
+ }
+ }
+ }
+ // At this point, `response` is either successful with a matching hash or is unsuccessful.
+ // Before returning it, check whether it failed with a 404 status. This would signify an
+ // unrecoverable state.
+ if (!response.ok && (response.status === 404)) {
+ throw new SwUnrecoverableStateError(`Failed to retrieve hashed resource from the server. (AssetGroup: ${this.config.name} | URL: ${url})`);
+ }
+ // Return the response (successful or unsuccessful).
+ return response;
+ }
+ else {
+ // This URL doesn't exist in our hash database, so it must be requested directly.
+ return this.safeFetch(req);
+ }
+ });
+ }
+ /**
+ * Possibly update a resource, if it's expired and needs to be updated. A no-op otherwise.
+ */
+ maybeUpdate(updateFrom, req, cache) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const url = this.adapter.normalizeUrl(req.url);
+ const meta = yield this.metadata;
+ // Check if this resource is hashed and already exists in the cache of a prior version.
+ if (this.hashes.has(url)) {
+ const hash = this.hashes.get(url);
+ // Check the caches of prior versions, using the hash to ensure the correct version of
+ // the resource is loaded.
+ const res = yield updateFrom.lookupResourceWithHash(url, hash);
+ // If a previously cached version was available, copy it over to this cache.
+ if (res !== null) {
+ // Copy to this cache.
+ yield cache.put(req, res);
+ yield meta.write(req.url, { ts: this.adapter.time, used: false });
+ // No need to do anything further with this resource, it's now cached properly.
+ return true;
+ }
+ }
+ // No up-to-date version of this resource could be found.
+ return false;
+ });
+ }
+ /**
+ * Construct a cache-busting URL for a given URL.
+ */
+ cacheBust(url) {
+ return url + (url.indexOf('?') === -1 ? '?' : '&') + 'ngsw-cache-bust=' + Math.random();
+ }
+ safeFetch(req) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ return yield this.scope.fetch(req);
+ }
+ catch (_a) {
+ return this.adapter.newResponse('', {
+ status: 504,
+ statusText: 'Gateway Timeout',
+ });
+ }
+ });
+ }
+ }
+ /**
+ * An `AssetGroup` that prefetches all of its resources during initialization.
+ */
+ class PrefetchAssetGroup extends AssetGroup {
+ initializeFully(updateFrom) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Open the cache which actually holds requests.
+ const cache = yield this.cache;
+ // Cache all known resources serially. As this reduce proceeds, each Promise waits
+ // on the last before starting the fetch/cache operation for the next request. Any
+ // errors cause fall-through to the final Promise which rejects.
+ yield this.urls.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
+ // Wait on all previous operations to complete.
+ yield previous;
+ // Construct the Request for this url.
+ const req = this.adapter.newRequest(url);
+ // First, check the cache to see if there is already a copy of this resource.
+ const alreadyCached = (yield cache.match(req, this.config.cacheQueryOptions)) !== undefined;
+ // If the resource is in the cache already, it can be skipped.
+ if (alreadyCached) {
+ return;
+ }
+ // If an update source is available.
+ if (updateFrom !== undefined && (yield this.maybeUpdate(updateFrom, req, cache))) {
+ return;
+ }
+ // Otherwise, go to the network and hopefully cache the response (if successful).
+ yield this.fetchAndCacheOnce(req, false);
+ }), Promise.resolve());
+ // Handle updating of unknown (unhashed) resources. This is only possible if there's
+ // a source to update from.
+ if (updateFrom !== undefined) {
+ const metaTable = yield this.metadata;
+ // Select all of the previously cached resources. These are cached unhashed resources
+ // from previous versions of the app, in any asset group.
+ yield (yield updateFrom.previouslyCachedResources())
+ // First, narrow down the set of resources to those which are handled by this group.
+ // Either it's a known URL, or it matches a given pattern.
+ .filter(url => this.urls.indexOf(url) !== -1 || this.patterns.some(pattern => pattern.test(url)))
+ // Finally, process each resource in turn.
+ .reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
+ yield previous;
+ const req = this.adapter.newRequest(url);
+ // It's possible that the resource in question is already cached. If so,
+ // continue to the next one.
+ const alreadyCached = ((yield cache.match(req, this.config.cacheQueryOptions)) !== undefined);
+ if (alreadyCached) {
+ return;
+ }
+ // Get the most recent old version of the resource.
+ const res = yield updateFrom.lookupResourceWithoutHash(url);
+ if (res === null || res.metadata === undefined) {
+ // Unexpected, but not harmful.
+ return;
+ }
+ // Write it into the cache. It may already be expired, but it can still serve
+ // traffic until it's updated (stale-while-revalidate approach).
+ yield cache.put(req, res.response);
+ yield metaTable.write(req.url, Object.assign(Object.assign({}, res.metadata), { used: false }));
+ }), Promise.resolve());
+ }
+ });
+ }
+ }
+ class LazyAssetGroup extends AssetGroup {
+ initializeFully(updateFrom) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // No action necessary if no update source is available - resources managed in this group
+ // are all lazily loaded, so there's nothing to initialize.
+ if (updateFrom === undefined) {
+ return;
+ }
+ // Open the cache which actually holds requests.
+ const cache = yield this.cache;
+ // Loop through the listed resources, caching any which are available.
+ yield this.urls.reduce((previous, url) => __awaiter(this, void 0, void 0, function* () {
+ // Wait on all previous operations to complete.
+ yield previous;
+ // Construct the Request for this url.
+ const req = this.adapter.newRequest(url);
+ // First, check the cache to see if there is already a copy of this resource.
+ const alreadyCached = (yield cache.match(req, this.config.cacheQueryOptions)) !== undefined;
+ // If the resource is in the cache already, it can be skipped.
+ if (alreadyCached) {
+ return;
+ }
+ const updated = yield this.maybeUpdate(updateFrom, req, cache);
+ if (this.config.updateMode === 'prefetch' && !updated) {
+ // If the resource was not updated, either it was not cached before or
+ // the previously cached version didn't match the updated hash. In that
+ // case, prefetch update mode dictates that the resource will be updated,
+ // except if it was not previously utilized. Check the status of the
+ // cached resource to see.
+ const cacheStatus = yield updateFrom.recentCacheStatus(url);
+ // If the resource is not cached, or was cached but unused, then it will be
+ // loaded lazily.
+ if (cacheStatus !== UpdateCacheStatus.CACHED) {
+ return;
+ }
+ // Update from the network.
+ yield this.fetchAndCacheOnce(req, false);
+ }
+ }), Promise.resolve());
+ });
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ /**
+ * Manages an instance of `LruState` and moves URLs to the head of the
+ * chain when requested.
+ */
+ class LruList {
+ constructor(state) {
+ if (state === undefined) {
+ state = {
+ head: null,
+ tail: null,
+ map: {},
+ count: 0,
+ };
+ }
+ this.state = state;
+ }
+ /**
+ * The current count of URLs in the list.
+ */
+ get size() {
+ return this.state.count;
+ }
+ /**
+ * Remove the tail.
+ */
+ pop() {
+ // If there is no tail, return null.
+ if (this.state.tail === null) {
+ return null;
+ }
+ const url = this.state.tail;
+ this.remove(url);
+ // This URL has been successfully evicted.
+ return url;
+ }
+ remove(url) {
+ const node = this.state.map[url];
+ if (node === undefined) {
+ return false;
+ }
+ // Special case if removing the current head.
+ if (this.state.head === url) {
+ // The node is the current head. Special case the removal.
+ if (node.next === null) {
+ // This is the only node. Reset the cache to be empty.
+ this.state.head = null;
+ this.state.tail = null;
+ this.state.map = {};
+ this.state.count = 0;
+ return true;
+ }
+ // There is at least one other node. Make the next node the new head.
+ const next = this.state.map[node.next];
+ next.previous = null;
+ this.state.head = next.url;
+ node.next = null;
+ delete this.state.map[url];
+ this.state.count--;
+ return true;
+ }
+ // The node is not the head, so it has a previous. It may or may not be the tail.
+ // If it is not, then it has a next. First, grab the previous node.
+ const previous = this.state.map[node.previous];
+ // Fix the forward pointer to skip over node and go directly to node.next.
+ previous.next = node.next;
+ // node.next may or may not be set. If it is, fix the back pointer to skip over node.
+ // If it's not set, then this node happened to be the tail, and the tail needs to be
+ // updated to point to the previous node (removing the tail).
+ if (node.next !== null) {
+ // There is a next node, fix its back pointer to skip this node.
+ this.state.map[node.next].previous = node.previous;
+ }
+ else {
+ // There is no next node - the accessed node must be the tail. Move the tail pointer.
+ this.state.tail = node.previous;
+ }
+ node.next = null;
+ node.previous = null;
+ delete this.state.map[url];
+ // Count the removal.
+ this.state.count--;
+ return true;
+ }
+ accessed(url) {
+ // When a URL is accessed, its node needs to be moved to the head of the chain.
+ // This is accomplished in two steps:
+ //
+ // 1) remove the node from its position within the chain.
+ // 2) insert the node as the new head.
+ //
+ // Sometimes, a URL is accessed which has not been seen before. In this case, step 1 can
+ // be skipped completely (which will grow the chain by one). Of course, if the node is
+ // already the head, this whole operation can be skipped.
+ if (this.state.head === url) {
+ // The URL is already in the head position, accessing it is a no-op.
+ return;
+ }
+ // Look up the node in the map, and construct a new entry if it's
+ const node = this.state.map[url] || { url, next: null, previous: null };
+ // Step 1: remove the node from its position within the chain, if it is in the chain.
+ if (this.state.map[url] !== undefined) {
+ this.remove(url);
+ }
+ // Step 2: insert the node at the head of the chain.
+ // First, check if there's an existing head node. If there is, it has previous: null.
+ // Its previous pointer should be set to the node we're inserting.
+ if (this.state.head !== null) {
+ this.state.map[this.state.head].previous = url;
+ }
+ // The next pointer of the node being inserted gets set to the old head, before the head
+ // pointer is updated to this node.
+ node.next = this.state.head;
+ // The new head is the new node.
+ this.state.head = url;
+ // If there is no tail, then this is the first node, and is both the head and the tail.
+ if (this.state.tail === null) {
+ this.state.tail = url;
+ }
+ // Set the node in the map of nodes (if the URL has been seen before, this is a no-op)
+ // and count the insertion.
+ this.state.map[url] = node;
+ this.state.count++;
+ }
+ }
+ /**
+ * A group of cached resources determined by a set of URL patterns which follow a LRU policy
+ * for caching.
+ */
+ class DataGroup {
+ constructor(scope, adapter, config, db, debugHandler, prefix) {
+ this.scope = scope;
+ this.adapter = adapter;
+ this.config = config;
+ this.db = db;
+ this.debugHandler = debugHandler;
+ this.prefix = prefix;
+ /**
+ * Tracks the LRU state of resources in this cache.
+ */
+ this._lru = null;
+ this.patterns = this.config.patterns.map(pattern => new RegExp(pattern));
+ this.cache = this.scope.caches.open(`${this.prefix}:dynamic:${this.config.name}:cache`);
+ this.lruTable = this.db.open(`${this.prefix}:dynamic:${this.config.name}:lru`, this.config.cacheQueryOptions);
+ this.ageTable = this.db.open(`${this.prefix}:dynamic:${this.config.name}:age`, this.config.cacheQueryOptions);
+ }
+ /**
+ * Lazily initialize/load the LRU chain.
+ */
+ lru() {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (this._lru === null) {
+ const table = yield this.lruTable;
+ try {
+ this._lru = new LruList(yield table.read('lru'));
+ }
+ catch (_a) {
+ this._lru = new LruList();
+ }
+ }
+ return this._lru;
+ });
+ }
+ /**
+ * Sync the LRU chain to non-volatile storage.
+ */
+ syncLru() {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (this._lru === null) {
+ return;
+ }
+ const table = yield this.lruTable;
+ try {
+ return table.write('lru', this._lru.state);
+ }
+ catch (err) {
+ // Writing lru cache table failed. This could be a result of a full storage.
+ // Continue serving clients as usual.
+ this.debugHandler.log(err, `DataGroup(${this.config.name}@${this.config.version}).syncLru()`);
+ // TODO: Better detect/handle full storage; e.g. using
+ // [navigator.storage](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorStorage/storage).
+ }
+ });
+ }
+ /**
+ * Process a fetch event and return a `Response` if the resource is covered by this group,
+ * or `null` otherwise.
+ */
+ handleFetch(req, ctx) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Do nothing
+ if (!this.patterns.some(pattern => pattern.test(req.url))) {
+ return null;
+ }
+ // Lazily initialize the LRU cache.
+ const lru = yield this.lru();
+ // The URL matches this cache. First, check whether this is a mutating request or not.
+ switch (req.method) {
+ case 'OPTIONS':
+ // Don't try to cache this - it's non-mutating, but is part of a mutating request.
+ // Most likely SWs don't even see this, but this guard is here just in case.
+ return null;
+ case 'GET':
+ case 'HEAD':
+ // Handle the request with whatever strategy was selected.
+ switch (this.config.strategy) {
+ case 'freshness':
+ return this.handleFetchWithFreshness(req, ctx, lru);
+ case 'performance':
+ return this.handleFetchWithPerformance(req, ctx, lru);
+ default:
+ throw new Error(`Unknown strategy: ${this.config.strategy}`);
+ }
+ default:
+ // This was a mutating request. Assume the cache for this URL is no longer valid.
+ const wasCached = lru.remove(req.url);
+ // If there was a cached entry, remove it.
+ if (wasCached) {
+ yield this.clearCacheForUrl(req.url);
+ }
+ // Sync the LRU chain to non-volatile storage.
+ yield this.syncLru();
+ // Finally, fall back on the network.
+ return this.safeFetch(req);
+ }
+ });
+ }
+ handleFetchWithPerformance(req, ctx, lru) {
+ return __awaiter(this, void 0, void 0, function* () {
+ let res = null;
+ // Check the cache first. If the resource exists there (and is not expired), the cached
+ // version can be used.
+ const fromCache = yield this.loadFromCache(req, lru);
+ if (fromCache !== null) {
+ res = fromCache.res;
+ // Check the age of the resource.
+ if (this.config.refreshAheadMs !== undefined && fromCache.age >= this.config.refreshAheadMs) {
+ ctx.waitUntil(this.safeCacheResponse(req, this.safeFetch(req), lru));
+ }
+ }
+ if (res !== null) {
+ return res;
+ }
+ // No match from the cache. Go to the network. Note that this is not an 'await'
+ // call, networkFetch is the actual Promise. This is due to timeout handling.
+ const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
+ res = yield timeoutFetch;
+ // Since fetch() will always return a response, undefined indicates a timeout.
+ if (res === undefined) {
+ // The request timed out. Return a Gateway Timeout error.
+ res = this.adapter.newResponse(null, { status: 504, statusText: 'Gateway Timeout' });
+ // Cache the network response eventually.
+ ctx.waitUntil(this.safeCacheResponse(req, networkFetch, lru));
+ }
+ else {
+ // The request completed in time, so cache it inline with the response flow.
+ yield this.safeCacheResponse(req, res, lru);
+ }
+ return res;
+ });
+ }
+ handleFetchWithFreshness(req, ctx, lru) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Start with a network fetch.
+ const [timeoutFetch, networkFetch] = this.networkFetchWithTimeout(req);
+ let res;
+ // If that fetch errors, treat it as a timed out request.
+ try {
+ res = yield timeoutFetch;
+ }
+ catch (_a) {
+ res = undefined;
+ }
+ // If the network fetch times out or errors, fall back on the cache.
+ if (res === undefined) {
+ ctx.waitUntil(this.safeCacheResponse(req, networkFetch, lru, true));
+ // Ignore the age, the network response will be cached anyway due to the
+ // behavior of freshness.
+ const fromCache = yield this.loadFromCache(req, lru);
+ res = (fromCache !== null) ? fromCache.res : null;
+ }
+ else {
+ yield this.safeCacheResponse(req, res, lru, true);
+ }
+ // Either the network fetch didn't time out, or the cache yielded a usable response.
+ // In either case, use it.
+ if (res !== null) {
+ return res;
+ }
+ // No response in the cache. No choice but to fall back on the full network fetch.
+ return networkFetch;
+ });
+ }
+ networkFetchWithTimeout(req) {
+ // If there is a timeout configured, race a timeout Promise with the network fetch.
+ // Otherwise, just fetch from the network directly.
+ if (this.config.timeoutMs !== undefined) {
+ const networkFetch = this.scope.fetch(req);
+ const safeNetworkFetch = (() => __awaiter(this, void 0, void 0, function* () {
+ try {
+ return yield networkFetch;
+ }
+ catch (_a) {
+ return this.adapter.newResponse(null, {
+ status: 504,
+ statusText: 'Gateway Timeout',
+ });
+ }
+ }))();
+ const networkFetchUndefinedError = (() => __awaiter(this, void 0, void 0, function* () {
+ try {
+ return yield networkFetch;
+ }
+ catch (_b) {
+ return undefined;
+ }
+ }))();
+ // Construct a Promise for the timeout.
+ const timeout = this.adapter.timeout(this.config.timeoutMs);
+ // Race that with the network fetch. This will either be a Response, or `undefined`
+ // in the event that the request errored or timed out.
+ return [Promise.race([networkFetchUndefinedError, timeout]), safeNetworkFetch];
+ }
+ else {
+ const networkFetch = this.safeFetch(req);
+ // Do a plain fetch.
+ return [networkFetch, networkFetch];
+ }
+ }
+ safeCacheResponse(req, resOrPromise, lru, okToCacheOpaque) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ const res = yield resOrPromise;
+ try {
+ yield this.cacheResponse(req, res, lru, okToCacheOpaque);
+ }
+ catch (err) {
+ // Saving the API response failed. This could be a result of a full storage.
+ // Since this data is cached lazily and temporarily, continue serving clients as usual.
+ this.debugHandler.log(err, `DataGroup(${this.config.name}@${this.config.version}).safeCacheResponse(${req.url}, status: ${res.status})`);
+ // TODO: Better detect/handle full storage; e.g. using
+ // [navigator.storage](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorStorage/storage).
+ }
+ }
+ catch (_a) {
+ // Request failed
+ // TODO: Handle this error somehow?
+ }
+ });
+ }
+ loadFromCache(req, lru) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Look for a response in the cache. If one exists, return it.
+ const cache = yield this.cache;
+ let res = yield cache.match(req, this.config.cacheQueryOptions);
+ if (res !== undefined) {
+ // A response was found in the cache, but its age is not yet known. Look it up.
+ try {
+ const ageTable = yield this.ageTable;
+ const age = this.adapter.time - (yield ageTable.read(req.url)).age;
+ // If the response is young enough, use it.
+ if (age <= this.config.maxAge) {
+ // Successful match from the cache. Use the response, after marking it as having
+ // been accessed.
+ lru.accessed(req.url);
+ return { res, age };
+ }
+ // Otherwise, or if there was an error, assume the response is expired, and evict it.
+ }
+ catch (_a) {
+ // Some error getting the age for the response. Assume it's expired.
+ }
+ lru.remove(req.url);
+ yield this.clearCacheForUrl(req.url);
+ // TODO: avoid duplicate in event of network timeout, maybe.
+ yield this.syncLru();
+ }
+ return null;
+ });
+ }
+ /**
+ * Operation for caching the response from the server. This has to happen all
+ * at once, so that the cache and LRU tracking remain in sync. If the network request
+ * completes before the timeout, this logic will be run inline with the response flow.
+ * If the request times out on the server, an error will be returned but the real network
+ * request will still be running in the background, to be cached when it completes.
+ */
+ cacheResponse(req, res, lru, okToCacheOpaque = false) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Only cache successful responses.
+ if (!(res.ok || (okToCacheOpaque && res.type === 'opaque'))) {
+ return;
+ }
+ // If caching this response would make the cache exceed its maximum size, evict something
+ // first.
+ if (lru.size >= this.config.maxSize) {
+ // The cache is too big, evict something.
+ const evictedUrl = lru.pop();
+ if (evictedUrl !== null) {
+ yield this.clearCacheForUrl(evictedUrl);
+ }
+ }
+ // TODO: evaluate for possible race conditions during flaky network periods.
+ // Mark this resource as having been accessed recently. This ensures it won't be evicted
+ // until enough other resources are requested that it falls off the end of the LRU chain.
+ lru.accessed(req.url);
+ // Store the response in the cache (cloning because the browser will consume
+ // the body during the caching operation).
+ yield (yield this.cache).put(req, res.clone());
+ // Store the age of the cache.
+ const ageTable = yield this.ageTable;
+ yield ageTable.write(req.url, { age: this.adapter.time });
+ // Sync the LRU chain to non-volatile storage.
+ yield this.syncLru();
+ });
+ }
+ /**
+ * Delete all of the saved state which this group uses to track resources.
+ */
+ cleanup() {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Remove both the cache and the database entries which track LRU stats.
+ yield Promise.all([
+ this.scope.caches.delete(`${this.prefix}:dynamic:${this.config.name}:cache`),
+ this.db.delete(`${this.prefix}:dynamic:${this.config.name}:age`),
+ this.db.delete(`${this.prefix}:dynamic:${this.config.name}:lru`),
+ ]);
+ });
+ }
+ /**
+ * Clear the state of the cache for a particular resource.
+ *
+ * This doesn't remove the resource from the LRU table, that is assumed to have
+ * been done already. This clears the GET and HEAD versions of the request from
+ * the cache itself, as well as the metadata stored in the age table.
+ */
+ clearCacheForUrl(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const [cache, ageTable] = yield Promise.all([this.cache, this.ageTable]);
+ yield Promise.all([
+ cache.delete(this.adapter.newRequest(url, { method: 'GET' }), this.config.cacheQueryOptions),
+ cache.delete(this.adapter.newRequest(url, { method: 'HEAD' }), this.config.cacheQueryOptions),
+ ageTable.delete(url),
+ ]);
+ });
+ }
+ safeFetch(req) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ return this.scope.fetch(req);
+ }
+ catch (_a) {
+ return this.adapter.newResponse(null, {
+ status: 504,
+ statusText: 'Gateway Timeout',
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ const BACKWARDS_COMPATIBILITY_NAVIGATION_URLS = [
+ { positive: true, regex: '^/.*$' },
+ { positive: false, regex: '^/.*\\.[^/]*$' },
+ { positive: false, regex: '^/.*__' },
+ ];
+ /**
+ * A specific version of the application, identified by a unique manifest
+ * as determined by its hash.
+ *
+ * Each `AppVersion` can be thought of as a published version of the app
+ * that can be installed as an update to any previously installed versions.
+ */
+ class AppVersion {
+ constructor(scope, adapter, database, idle, debugHandler, manifest, manifestHash) {
+ this.scope = scope;
+ this.adapter = adapter;
+ this.database = database;
+ this.idle = idle;
+ this.debugHandler = debugHandler;
+ this.manifest = manifest;
+ this.manifestHash = manifestHash;
+ /**
+ * A Map of absolute URL paths (`/foo.txt`) to the known hash of their contents (if available).
+ */
+ this.hashTable = new Map();
+ /**
+ * The normalized URL to the file that serves as the index page to satisfy navigation requests.
+ * Usually this is `/index.html`.
+ */
+ this.indexUrl = this.adapter.normalizeUrl(this.manifest.index);
+ /**
+ * Tracks whether the manifest has encountered any inconsistencies.
+ */
+ this._okay = true;
+ // The hashTable within the manifest is an Object - convert it to a Map for easier lookups.
+ Object.keys(this.manifest.hashTable).forEach(url => {
+ this.hashTable.set(adapter.normalizeUrl(url), this.manifest.hashTable[url]);
+ });
+ // Process each `AssetGroup` declared in the manifest. Each declared group gets an `AssetGroup`
+ // instance
+ // created for it, of a type that depends on the configuration mode.
+ this.assetGroups = (manifest.assetGroups || []).map(config => {
+ // Every asset group has a cache that's prefixed by the manifest hash and the name of the
+ // group.
+ const prefix = `${adapter.cacheNamePrefix}:${this.manifestHash}:assets`;
+ // Check the caching mode, which determines when resources will be fetched/updated.
+ switch (config.installMode) {
+ case 'prefetch':
+ return new PrefetchAssetGroup(this.scope, this.adapter, this.idle, config, this.hashTable, this.database, prefix);
+ case 'lazy':
+ return new LazyAssetGroup(this.scope, this.adapter, this.idle, config, this.hashTable, this.database, prefix);
+ }
+ });
+ // Process each `DataGroup` declared in the manifest.
+ this.dataGroups =
+ (manifest.dataGroups || [])
+ .map(config => new DataGroup(this.scope, this.adapter, config, this.database, this.debugHandler, `${adapter.cacheNamePrefix}:${config.version}:data`));
+ // This keeps backwards compatibility with app versions without navigation urls.
+ // Fix: https://github.com/angular/angular/issues/27209
+ manifest.navigationUrls = manifest.navigationUrls || BACKWARDS_COMPATIBILITY_NAVIGATION_URLS;
+ // Create `include`/`exclude` RegExps for the `navigationUrls` declared in the manifest.
+ const includeUrls = manifest.navigationUrls.filter(spec => spec.positive);
+ const excludeUrls = manifest.navigationUrls.filter(spec => !spec.positive);
+ this.navigationUrls = {
+ include: includeUrls.map(spec => new RegExp(spec.regex)),
+ exclude: excludeUrls.map(spec => new RegExp(spec.regex)),
+ };
+ }
+ get okay() {
+ return this._okay;
+ }
+ /**
+ * Fully initialize this version of the application. If this Promise resolves successfully, all
+ * required
+ * data has been safely downloaded.
+ */
+ initializeFully(updateFrom) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ // Fully initialize each asset group, in series. Starts with an empty Promise,
+ // and waits for the previous groups to have been initialized before initializing
+ // the next one in turn.
+ yield this.assetGroups.reduce((previous, group) => __awaiter(this, void 0, void 0, function* () {
+ // Wait for the previous groups to complete initialization. If there is a
+ // failure, this will throw, and each subsequent group will throw, until the
+ // whole sequence fails.
+ yield previous;
+ // Initialize this group.
+ return group.initializeFully(updateFrom);
+ }), Promise.resolve());
+ }
+ catch (err) {
+ this._okay = false;
+ throw err;
+ }
+ });
+ }
+ handleFetch(req, context) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Check the request against each `AssetGroup` in sequence. If an `AssetGroup` can't handle the
+ // request,
+ // it will return `null`. Thus, the first non-null response is the SW's answer to the request.
+ // So reduce
+ // the group list, keeping track of a possible response. If there is one, it gets passed
+ // through, and if
+ // not the next group is consulted to produce a candidate response.
+ const asset = yield this.assetGroups.reduce((potentialResponse, group) => __awaiter(this, void 0, void 0, function* () {
+ // Wait on the previous potential response. If it's not null, it should just be passed
+ // through.
+ const resp = yield potentialResponse;
+ if (resp !== null) {
+ return resp;
+ }
+ // No response has been found yet. Maybe this group will have one.
+ return group.handleFetch(req, context);
+ }), Promise.resolve(null));
+ // The result of the above is the asset response, if there is any, or null otherwise. Return the
+ // asset
+ // response if there was one. If not, check with the data caching groups.
+ if (asset !== null) {
+ return asset;
+ }
+ // Perform the same reduction operation as above, but this time processing
+ // the data caching groups.
+ const data = yield this.dataGroups.reduce((potentialResponse, group) => __awaiter(this, void 0, void 0, function* () {
+ const resp = yield potentialResponse;
+ if (resp !== null) {
+ return resp;
+ }
+ return group.handleFetch(req, context);
+ }), Promise.resolve(null));
+ // If the data caching group returned a response, go with it.
+ if (data !== null) {
+ return data;
+ }
+ // Next, check if this is a navigation request for a route. Detect circular
+ // navigations by checking if the request URL is the same as the index URL.
+ if (this.adapter.normalizeUrl(req.url) !== this.indexUrl && this.isNavigationRequest(req)) {
+ if (this.manifest.navigationRequestStrategy === 'freshness') {
+ // For navigation requests the freshness was configured. The request will always go trough
+ // the network and fallback to default `handleFetch` behavior in case of failure.
+ try {
+ return yield this.scope.fetch(req);
+ }
+ catch (_a) {
+ // Navigation request failed - application is likely offline.
+ // Proceed forward to the default `handleFetch` behavior, where
+ // `indexUrl` will be requested and it should be available in the cache.
+ }
+ }
+ // This was a navigation request. Re-enter `handleFetch` with a request for
+ // the URL.
+ return this.handleFetch(this.adapter.newRequest(this.indexUrl), context);
+ }
+ return null;
+ });
+ }
+ /**
+ * Determine whether the request is a navigation request.
+ * Takes into account: Request mode, `Accept` header, `navigationUrls` patterns.
+ */
+ isNavigationRequest(req) {
+ if (req.mode !== 'navigate') {
+ return false;
+ }
+ if (!this.acceptsTextHtml(req)) {
+ return false;
+ }
+ const urlPrefix = this.scope.registration.scope.replace(/\/$/, '');
+ const url = req.url.startsWith(urlPrefix) ? req.url.substr(urlPrefix.length) : req.url;
+ const urlWithoutQueryOrHash = url.replace(/[?#].*$/, '');
+ return this.navigationUrls.include.some(regex => regex.test(urlWithoutQueryOrHash)) &&
+ !this.navigationUrls.exclude.some(regex => regex.test(urlWithoutQueryOrHash));
+ }
+ /**
+ * Check this version for a given resource with a particular hash.
+ */
+ lookupResourceWithHash(url, hash) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Verify that this version has the requested resource cached. If not,
+ // there's no point in trying.
+ if (!this.hashTable.has(url)) {
+ return null;
+ }
+ // Next, check whether the resource has the correct hash. If not, any cached
+ // response isn't usable.
+ if (this.hashTable.get(url) !== hash) {
+ return null;
+ }
+ const cacheState = yield this.lookupResourceWithoutHash(url);
+ return cacheState && cacheState.response;
+ });
+ }
+ /**
+ * Check this version for a given resource regardless of its hash.
+ */
+ lookupResourceWithoutHash(url) {
+ // Limit the search to asset groups, and only scan the cache, don't
+ // load resources from the network.
+ return this.assetGroups.reduce((potentialResponse, group) => __awaiter(this, void 0, void 0, function* () {
+ const resp = yield potentialResponse;
+ if (resp !== null) {
+ return resp;
+ }
+ // fetchFromCacheOnly() avoids any network fetches, and returns the
+ // full set of cache data, not just the Response.
+ return group.fetchFromCacheOnly(url);
+ }), Promise.resolve(null));
+ }
+ /**
+ * List all unhashed resources from all asset groups.
+ */
+ previouslyCachedResources() {
+ return this.assetGroups.reduce((resources, group) => __awaiter(this, void 0, void 0, function* () { return (yield resources).concat(yield group.unhashedResources()); }), Promise.resolve([]));
+ }
+ recentCacheStatus(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.assetGroups.reduce((current, group) => __awaiter(this, void 0, void 0, function* () {
+ const status = yield current;
+ if (status === UpdateCacheStatus.CACHED) {
+ return status;
+ }
+ const groupStatus = yield group.cacheStatus(url);
+ if (groupStatus === UpdateCacheStatus.NOT_CACHED) {
+ return status;
+ }
+ return groupStatus;
+ }), Promise.resolve(UpdateCacheStatus.NOT_CACHED));
+ });
+ }
+ /**
+ * Erase this application version, by cleaning up all the caches.
+ */
+ cleanup() {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield Promise.all(this.assetGroups.map(group => group.cleanup()));
+ yield Promise.all(this.dataGroups.map(group => group.cleanup()));
+ });
+ }
+ /**
+ * Get the opaque application data which was provided with the manifest.
+ */
+ get appData() {
+ return this.manifest.appData || null;
+ }
+ /**
+ * Check whether a request accepts `text/html` (based on the `Accept` header).
+ */
+ acceptsTextHtml(req) {
+ const accept = req.headers.get('Accept');
+ if (accept === null) {
+ return false;
+ }
+ const values = accept.split(',');
+ return values.some(value => value.trim().toLowerCase() === 'text/html');
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ const DEBUG_LOG_BUFFER_SIZE = 100;
+ class DebugHandler {
+ constructor(driver, adapter) {
+ this.driver = driver;
+ this.adapter = adapter;
+ // There are two debug log message arrays. debugLogA records new debugging messages.
+ // Once it reaches DEBUG_LOG_BUFFER_SIZE, the array is moved to debugLogB and a new
+ // array is assigned to debugLogA. This ensures that insertion to the debug log is
+ // always O(1) no matter the number of logged messages, and that the total number
+ // of messages in the log never exceeds 2 * DEBUG_LOG_BUFFER_SIZE.
+ this.debugLogA = [];
+ this.debugLogB = [];
+ }
+ handleFetch(req) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const [state, versions, idle] = yield Promise.all([
+ this.driver.debugState(),
+ this.driver.debugVersions(),
+ this.driver.debugIdleState(),
+ ]);
+ const msgState = `NGSW Debug Info:
+
+Driver state: ${state.state} (${state.why})
+Latest manifest hash: ${state.latestHash || 'none'}
+Last update check: ${this.since(state.lastUpdateCheck)}`;
+ const msgVersions = versions
+ .map(version => `=== Version ${version.hash} ===
+
+Clients: ${version.clients.join(', ')}`)
+ .join('\n\n');
+ const msgIdle = `=== Idle Task Queue ===
+Last update tick: ${this.since(idle.lastTrigger)}
+Last update run: ${this.since(idle.lastRun)}
+Task queue:
+${idle.queue.map(v => ' * ' + v).join('\n')}
+
+Debug log:
+${this.formatDebugLog(this.debugLogB)}
+${this.formatDebugLog(this.debugLogA)}
+`;
+ return this.adapter.newResponse(`${msgState}
+
+${msgVersions}
+
+${msgIdle}`, { headers: this.adapter.newHeaders({ 'Content-Type': 'text/plain' }) });
+ });
+ }
+ since(time) {
+ if (time === null) {
+ return 'never';
+ }
+ let age = this.adapter.time - time;
+ const days = Math.floor(age / 86400000);
+ age = age % 86400000;
+ const hours = Math.floor(age / 3600000);
+ age = age % 3600000;
+ const minutes = Math.floor(age / 60000);
+ age = age % 60000;
+ const seconds = Math.floor(age / 1000);
+ const millis = age % 1000;
+ return '' + (days > 0 ? `${days}d` : '') + (hours > 0 ? `${hours}h` : '') +
+ (minutes > 0 ? `${minutes}m` : '') + (seconds > 0 ? `${seconds}s` : '') +
+ (millis > 0 ? `${millis}u` : '');
+ }
+ log(value, context = '') {
+ // Rotate the buffers if debugLogA has grown too large.
+ if (this.debugLogA.length === DEBUG_LOG_BUFFER_SIZE) {
+ this.debugLogB = this.debugLogA;
+ this.debugLogA = [];
+ }
+ // Convert errors to string for logging.
+ if (typeof value !== 'string') {
+ value = this.errorToString(value);
+ }
+ // Log the message.
+ this.debugLogA.push({ value, time: this.adapter.time, context });
+ }
+ errorToString(err) {
+ return `${err.name}(${err.message}, ${err.stack})`;
+ }
+ formatDebugLog(log) {
+ return log.map(entry => `[${this.since(entry.time)}] ${entry.value} ${entry.context}`)
+ .join('\n');
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ class IdleScheduler {
+ constructor(adapter, delay, maxDelay, debug) {
+ this.adapter = adapter;
+ this.delay = delay;
+ this.maxDelay = maxDelay;
+ this.debug = debug;
+ this.queue = [];
+ this.scheduled = null;
+ this.empty = Promise.resolve();
+ this.emptyResolve = null;
+ this.lastTrigger = null;
+ this.lastRun = null;
+ this.oldestScheduledAt = null;
+ }
+ trigger() {
+ var _a;
+ return __awaiter(this, void 0, void 0, function* () {
+ this.lastTrigger = this.adapter.time;
+ if (this.queue.length === 0) {
+ return;
+ }
+ if (this.scheduled !== null) {
+ this.scheduled.cancel = true;
+ }
+ const scheduled = {
+ cancel: false,
+ };
+ this.scheduled = scheduled;
+ // Ensure that no task remains pending for longer than `this.maxDelay` ms.
+ const now = this.adapter.time;
+ const maxDelay = Math.max(0, ((_a = this.oldestScheduledAt) !== null && _a !== void 0 ? _a : now) + this.maxDelay - now);
+ const delay = Math.min(maxDelay, this.delay);
+ yield this.adapter.timeout(delay);
+ if (scheduled.cancel) {
+ return;
+ }
+ this.scheduled = null;
+ yield this.execute();
+ });
+ }
+ execute() {
+ return __awaiter(this, void 0, void 0, function* () {
+ this.lastRun = this.adapter.time;
+ while (this.queue.length > 0) {
+ const queue = this.queue;
+ this.queue = [];
+ yield queue.reduce((previous, task) => __awaiter(this, void 0, void 0, function* () {
+ yield previous;
+ try {
+ yield task.run();
+ }
+ catch (err) {
+ this.debug.log(err, `while running idle task ${task.desc}`);
+ }
+ }), Promise.resolve());
+ }
+ if (this.emptyResolve !== null) {
+ this.emptyResolve();
+ this.emptyResolve = null;
+ }
+ this.empty = Promise.resolve();
+ this.oldestScheduledAt = null;
+ });
+ }
+ schedule(desc, run) {
+ this.queue.push({ desc, run });
+ if (this.emptyResolve === null) {
+ this.empty = new Promise(resolve => {
+ this.emptyResolve = resolve;
+ });
+ }
+ if (this.oldestScheduledAt === null) {
+ this.oldestScheduledAt = this.adapter.time;
+ }
+ }
+ get size() {
+ return this.queue.length;
+ }
+ get taskDescriptions() {
+ return this.queue.map(task => task.desc);
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ function hashManifest(manifest) {
+ return sha1(JSON.stringify(manifest));
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ function isMsgCheckForUpdates(msg) {
+ return msg.action === 'CHECK_FOR_UPDATES';
+ }
+ function isMsgActivateUpdate(msg) {
+ return msg.action === 'ACTIVATE_UPDATE';
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ const IDLE_DELAY = 5000;
+ const MAX_IDLE_DELAY = 30000;
+ const SUPPORTED_CONFIG_VERSION = 1;
+ const NOTIFICATION_OPTION_NAMES = [
+ 'actions', 'badge', 'body', 'data', 'dir', 'icon', 'image', 'lang', 'renotify',
+ 'requireInteraction', 'silent', 'tag', 'timestamp', 'title', 'vibrate'
+ ];
+ var DriverReadyState = /*@__PURE__*/ (function (DriverReadyState) {
+ // The SW is operating in a normal mode, responding to all traffic.
+ DriverReadyState[DriverReadyState["NORMAL"] = 0] = "NORMAL";
+ // The SW does not have a clean installation of the latest version of the app, but older
+ // cached versions are safe to use so long as they don't try to fetch new dependencies.
+ // This is a degraded state.
+ DriverReadyState[DriverReadyState["EXISTING_CLIENTS_ONLY"] = 1] = "EXISTING_CLIENTS_ONLY";
+ // The SW has decided that caching is completely unreliable, and is forgoing request
+ // handling until the next restart.
+ DriverReadyState[DriverReadyState["SAFE_MODE"] = 2] = "SAFE_MODE";
+ return DriverReadyState;
+ })({});
+ class Driver {
+ constructor(scope, adapter, db) {
+ // Set up all the event handlers that the SW needs.
+ this.scope = scope;
+ this.adapter = adapter;
+ this.db = db;
+ /**
+ * Tracks the current readiness condition under which the SW is operating. This controls
+ * whether the SW attempts to respond to some or all requests.
+ */
+ this.state = DriverReadyState.NORMAL;
+ this.stateMessage = '(nominal)';
+ /**
+ * Tracks whether the SW is in an initialized state or not. Before initialization,
+ * it's not legal to respond to requests.
+ */
+ this.initialized = null;
+ /**
+ * Maps client IDs to the manifest hash of the application version being used to serve
+ * them. If a client ID is not present here, it has not yet been assigned a version.
+ *
+ * If a ManifestHash appears here, it is also present in the `versions` map below.
+ */
+ this.clientVersionMap = new Map();
+ /**
+ * Maps manifest hashes to instances of `AppVersion` for those manifests.
+ */
+ this.versions = new Map();
+ /**
+ * The latest version fetched from the server.
+ *
+ * Valid after initialization has completed.
+ */
+ this.latestHash = null;
+ this.lastUpdateCheck = null;
+ /**
+ * Whether there is a check for updates currently scheduled due to navigation.
+ */
+ this.scheduledNavUpdateCheck = false;
+ /**
+ * Keep track of whether we have logged an invalid `only-if-cached` request.
+ * (See `.onFetch()` for details.)
+ */
+ this.loggedInvalidOnlyIfCachedRequest = false;
+ this.ngswStatePath = this.adapter.parseUrl('ngsw/state', this.scope.registration.scope).path;
+ // The install event is triggered when the service worker is first installed.
+ this.scope.addEventListener('install', (event) => {
+ // SW code updates are separate from application updates, so code updates are
+ // almost as straightforward as restarting the SW. Because of this, it's always
+ // safe to skip waiting until application tabs are closed, and activate the new
+ // SW version immediately.
+ event.waitUntil(this.scope.skipWaiting());
+ });
+ // The activate event is triggered when this version of the service worker is
+ // first activated.
+ this.scope.addEventListener('activate', (event) => {
+ event.waitUntil((() => __awaiter(this, void 0, void 0, function* () {
+ // As above, it's safe to take over from existing clients immediately, since the new SW
+ // version will continue to serve the old application.
+ yield this.scope.clients.claim();
+ // Once all clients have been taken over, we can delete caches used by old versions of
+ // `@angular/service-worker`, which are no longer needed. This can happen in the background.
+ this.idle.schedule('activate: cleanup-old-sw-caches', () => __awaiter(this, void 0, void 0, function* () {
+ try {
+ yield this.cleanupOldSwCaches();
+ }
+ catch (err) {
+ // Nothing to do - cleanup failed. Just log it.
+ this.debugger.log(err, 'cleanupOldSwCaches @ activate: cleanup-old-sw-caches');
+ }
+ }));
+ }))());
+ // Rather than wait for the first fetch event, which may not arrive until
+ // the next time the application is loaded, the SW takes advantage of the
+ // activation event to schedule initialization. However, if this were run
+ // in the context of the 'activate' event, waitUntil() here would cause fetch
+ // events to block until initialization completed. Thus, the SW does a
+ // postMessage() to itself, to schedule a new event loop iteration with an
+ // entirely separate event context. The SW will be kept alive by waitUntil()
+ // within that separate context while initialization proceeds, while at the
+ // same time the activation event is allowed to resolve and traffic starts
+ // being served.
+ if (this.scope.registration.active !== null) {
+ this.scope.registration.active.postMessage({ action: 'INITIALIZE' });
+ }
+ });
+ // Handle the fetch, message, and push events.
+ this.scope.addEventListener('fetch', (event) => this.onFetch(event));
+ this.scope.addEventListener('message', (event) => this.onMessage(event));
+ this.scope.addEventListener('push', (event) => this.onPush(event));
+ this.scope.addEventListener('notificationclick', (event) => this.onClick(event));
+ // The debugger generates debug pages in response to debugging requests.
+ this.debugger = new DebugHandler(this, this.adapter);
+ // The IdleScheduler will execute idle tasks after a given delay.
+ this.idle = new IdleScheduler(this.adapter, IDLE_DELAY, MAX_IDLE_DELAY, this.debugger);
+ }
+ /**
+ * The handler for fetch events.
+ *
+ * This is the transition point between the synchronous event handler and the
+ * asynchronous execution that eventually resolves for respondWith() and waitUntil().
+ */
+ onFetch(event) {
+ const req = event.request;
+ const scopeUrl = this.scope.registration.scope;
+ const requestUrlObj = this.adapter.parseUrl(req.url, scopeUrl);
+ if (req.headers.has('ngsw-bypass') || /[?&]ngsw-bypass(?:[=&]|$)/i.test(requestUrlObj.search)) {
+ return;
+ }
+ // The only thing that is served unconditionally is the debug page.
+ if (requestUrlObj.path === this.ngswStatePath) {
+ // Allow the debugger to handle the request, but don't affect SW state in any other way.
+ event.respondWith(this.debugger.handleFetch(req));
+ return;
+ }
+ // If the SW is in a broken state where it's not safe to handle requests at all,
+ // returning causes the request to fall back on the network. This is preferred over
+ // `respondWith(fetch(req))` because the latter still shows in DevTools that the
+ // request was handled by the SW.
+ if (this.state === DriverReadyState.SAFE_MODE) {
+ // Even though the worker is in safe mode, idle tasks still need to happen so
+ // things like update checks, etc. can take place.
+ event.waitUntil(this.idle.trigger());
+ return;
+ }
+ // Although "passive mixed content" (like images) only produces a warning without a
+ // ServiceWorker, fetching it via a ServiceWorker results in an error. Let such requests be
+ // handled by the browser, since handling with the ServiceWorker would fail anyway.
+ // See https://github.com/angular/angular/issues/23012#issuecomment-376430187 for more details.
+ if (requestUrlObj.origin.startsWith('http:') && scopeUrl.startsWith('https:')) {
+ // Still, log the incident for debugging purposes.
+ this.debugger.log(`Ignoring passive mixed content request: Driver.fetch(${req.url})`);
+ return;
+ }
+ // When opening DevTools in Chrome, a request is made for the current URL (and possibly related
+ // resources, e.g. scripts) with `cache: 'only-if-cached'` and `mode: 'no-cors'`. These request
+ // will eventually fail, because `only-if-cached` is only allowed to be used with
+ // `mode: 'same-origin'`.
+ // This is likely a bug in Chrome DevTools. Avoid handling such requests.
+ // (See also https://github.com/angular/angular/issues/22362.)
+ // TODO(gkalpak): Remove once no longer necessary (i.e. fixed in Chrome DevTools).
+ if (req.cache === 'only-if-cached' && req.mode !== 'same-origin') {
+ // Log the incident only the first time it happens, to avoid spamming the logs.
+ if (!this.loggedInvalidOnlyIfCachedRequest) {
+ this.loggedInvalidOnlyIfCachedRequest = true;
+ this.debugger.log(`Ignoring invalid request: 'only-if-cached' can be set only with 'same-origin' mode`, `Driver.fetch(${req.url}, cache: ${req.cache}, mode: ${req.mode})`);
+ }
+ return;
+ }
+ // Past this point, the SW commits to handling the request itself. This could still
+ // fail (and result in `state` being set to `SAFE_MODE`), but even in that case the
+ // SW will still deliver a response.
+ event.respondWith(this.handleFetch(event));
+ }
+ /**
+ * The handler for message events.
+ */
+ onMessage(event) {
+ // Ignore message events when the SW is in safe mode, for now.
+ if (this.state === DriverReadyState.SAFE_MODE) {
+ return;
+ }
+ // If the message doesn't have the expected signature, ignore it.
+ const data = event.data;
+ if (!data || !data.action) {
+ return;
+ }
+ event.waitUntil((() => __awaiter(this, void 0, void 0, function* () {
+ // Initialization is the only event which is sent directly from the SW to itself, and thus
+ // `event.source` is not a `Client`. Handle it here, before the check for `Client` sources.
+ if (data.action === 'INITIALIZE') {
+ return this.ensureInitialized(event);
+ }
+ // Only messages from true clients are accepted past this point.
+ // This is essentially a typecast.
+ if (!this.adapter.isClient(event.source)) {
+ return;
+ }
+ // Handle the message and keep the SW alive until it's handled.
+ yield this.ensureInitialized(event);
+ yield this.handleMessage(data, event.source);
+ }))());
+ }
+ onPush(msg) {
+ // Push notifications without data have no effect.
+ if (!msg.data) {
+ return;
+ }
+ // Handle the push and keep the SW alive until it's handled.
+ msg.waitUntil(this.handlePush(msg.data.json()));
+ }
+ onClick(event) {
+ // Handle the click event and keep the SW alive until it's handled.
+ event.waitUntil(this.handleClick(event.notification, event.action));
+ }
+ ensureInitialized(event) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Since the SW may have just been started, it may or may not have been initialized already.
+ // `this.initialized` will be `null` if initialization has not yet been attempted, or will be a
+ // `Promise` which will resolve (successfully or unsuccessfully) if it has.
+ if (this.initialized !== null) {
+ return this.initialized;
+ }
+ // Initialization has not yet been attempted, so attempt it. This should only ever happen once
+ // per SW instantiation.
+ try {
+ this.initialized = this.initialize();
+ yield this.initialized;
+ }
+ catch (error) {
+ // If initialization fails, the SW needs to enter a safe state, where it declines to respond
+ // to network requests.
+ this.state = DriverReadyState.SAFE_MODE;
+ this.stateMessage = `Initialization failed due to error: ${errorToString(error)}`;
+ throw error;
+ }
+ finally {
+ // Regardless if initialization succeeded, background tasks still need to happen.
+ event.waitUntil(this.idle.trigger());
+ }
+ });
+ }
+ handleMessage(msg, from) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (isMsgCheckForUpdates(msg)) {
+ const action = (() => __awaiter(this, void 0, void 0, function* () {
+ yield this.checkForUpdate();
+ }))();
+ yield this.reportStatus(from, action, msg.statusNonce);
+ }
+ else if (isMsgActivateUpdate(msg)) {
+ yield this.reportStatus(from, this.updateClient(from), msg.statusNonce);
+ }
+ });
+ }
+ handlePush(data) {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.broadcast({
+ type: 'PUSH',
+ data,
+ });
+ if (!data.notification || !data.notification.title) {
+ return;
+ }
+ const desc = data.notification;
+ let options = {};
+ NOTIFICATION_OPTION_NAMES.filter(name => desc.hasOwnProperty(name))
+ .forEach(name => options[name] = desc[name]);
+ yield this.scope.registration.showNotification(desc['title'], options);
+ });
+ }
+ handleClick(notification, action) {
+ return __awaiter(this, void 0, void 0, function* () {
+ notification.close();
+ const options = {};
+ // The filter uses `name in notification` because the properties are on the prototype so
+ // hasOwnProperty does not work here
+ NOTIFICATION_OPTION_NAMES.filter(name => name in notification)
+ .forEach(name => options[name] = notification[name]);
+ yield this.broadcast({
+ type: 'NOTIFICATION_CLICK',
+ data: { action, notification: options },
+ });
+ });
+ }
+ reportStatus(client, promise, nonce) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const response = { type: 'STATUS', nonce, status: true };
+ try {
+ yield promise;
+ client.postMessage(response);
+ }
+ catch (e) {
+ client.postMessage(Object.assign(Object.assign({}, response), { status: false, error: e.toString() }));
+ }
+ });
+ }
+ updateClient(client) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Figure out which version the client is on. If it's not on the latest,
+ // it needs to be moved.
+ const existing = this.clientVersionMap.get(client.id);
+ if (existing === this.latestHash) {
+ // Nothing to do, this client is already on the latest version.
+ return;
+ }
+ // Switch the client over.
+ let previous = undefined;
+ // Look up the application data associated with the existing version. If there
+ // isn't any, fall back on using the hash.
+ if (existing !== undefined) {
+ const existingVersion = this.versions.get(existing);
+ previous = this.mergeHashWithAppData(existingVersion.manifest, existing);
+ }
+ // Set the current version used by the client, and sync the mapping to disk.
+ this.clientVersionMap.set(client.id, this.latestHash);
+ yield this.sync();
+ // Notify the client about this activation.
+ const current = this.versions.get(this.latestHash);
+ const notice = {
+ type: 'UPDATE_ACTIVATED',
+ previous,
+ current: this.mergeHashWithAppData(current.manifest, this.latestHash),
+ };
+ client.postMessage(notice);
+ });
+ }
+ handleFetch(event) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ // Ensure the SW instance has been initialized.
+ yield this.ensureInitialized(event);
+ }
+ catch (_a) {
+ // Since the SW is already committed to responding to the currently active request,
+ // respond with a network fetch.
+ return this.safeFetch(event.request);
+ }
+ // On navigation requests, check for new updates.
+ if (event.request.mode === 'navigate' && !this.scheduledNavUpdateCheck) {
+ this.scheduledNavUpdateCheck = true;
+ this.idle.schedule('check-updates-on-navigation', () => __awaiter(this, void 0, void 0, function* () {
+ this.scheduledNavUpdateCheck = false;
+ yield this.checkForUpdate();
+ }));
+ }
+ // Decide which version of the app to use to serve this request. This is asynchronous as in
+ // some cases, a record will need to be written to disk about the assignment that is made.
+ const appVersion = yield this.assignVersion(event);
+ let res = null;
+ try {
+ if (appVersion !== null) {
+ try {
+ // Handle the request. First try the AppVersion. If that doesn't work, fall back on the
+ // network.
+ res = yield appVersion.handleFetch(event.request, event);
+ }
+ catch (err) {
+ if (err.isUnrecoverableState) {
+ yield this.notifyClientsAboutUnrecoverableState(appVersion, err.message);
+ }
+ if (err.isCritical) {
+ // Something went wrong with the activation of this version.
+ yield this.versionFailed(appVersion, err);
+ return this.safeFetch(event.request);
+ }
+ throw err;
+ }
+ }
+ // The response will be `null` only if no `AppVersion` can be assigned to the request or if
+ // the assigned `AppVersion`'s manifest doesn't specify what to do about the request.
+ // In that case, just fall back on the network.
+ if (res === null) {
+ return this.safeFetch(event.request);
+ }
+ // The `AppVersion` returned a usable response, so return it.
+ return res;
+ }
+ finally {
+ // Trigger the idle scheduling system. The Promise returned by `trigger()` will resolve after
+ // a specific amount of time has passed. If `trigger()` hasn't been called again by then (e.g.
+ // on a subsequent request), the idle task queue will be drained and the `Promise` won't
+ // be resolved until that operation is complete as well.
+ event.waitUntil(this.idle.trigger());
+ }
+ });
+ }
+ /**
+ * Attempt to quickly reach a state where it's safe to serve responses.
+ */
+ initialize() {
+ return __awaiter(this, void 0, void 0, function* () {
+ // On initialization, all of the serialized state is read out of the 'control'
+ // table. This includes:
+ // - map of hashes to manifests of currently loaded application versions
+ // - map of client IDs to their pinned versions
+ // - record of the most recently fetched manifest hash
+ //
+ // If these values don't exist in the DB, then this is the either the first time
+ // the SW has run or the DB state has been wiped or is inconsistent. In that case,
+ // load a fresh copy of the manifest and reset the state from scratch.
+ // Open up the DB table.
+ const table = yield this.db.open('control');
+ // Attempt to load the needed state from the DB. If this fails, the catch {} block
+ // will populate these variables with freshly constructed values.
+ let manifests, assignments, latest;
+ try {
+ // Read them from the DB simultaneously.
+ [manifests, assignments, latest] = yield Promise.all([
+ table.read('manifests'),
+ table.read('assignments'),
+ table.read('latest'),
+ ]);
+ // Make sure latest manifest is correctly installed. If not (e.g. corrupted data),
+ // it could stay locked in EXISTING_CLIENTS_ONLY or SAFE_MODE state.
+ if (!this.versions.has(latest.latest) && !manifests.hasOwnProperty(latest.latest)) {
+ this.debugger.log(`Missing manifest for latest version hash ${latest.latest}`, 'initialize: read from DB');
+ throw new Error(`Missing manifest for latest hash ${latest.latest}`);
+ }
+ // Successfully loaded from saved state. This implies a manifest exists, so
+ // the update check needs to happen in the background.
+ this.idle.schedule('init post-load (update, cleanup)', () => __awaiter(this, void 0, void 0, function* () {
+ yield this.checkForUpdate();
+ try {
+ yield this.cleanupCaches();
+ }
+ catch (err) {
+ // Nothing to do - cleanup failed. Just log it.
+ this.debugger.log(err, 'cleanupCaches @ init post-load');
+ }
+ }));
+ }
+ catch (_) {
+ // Something went wrong. Try to start over by fetching a new manifest from the
+ // server and building up an empty initial state.
+ const manifest = yield this.fetchLatestManifest();
+ const hash = hashManifest(manifest);
+ manifests = {};
+ manifests[hash] = manifest;
+ assignments = {};
+ latest = { latest: hash };
+ // Save the initial state to the DB.
+ yield Promise.all([
+ table.write('manifests', manifests),
+ table.write('assignments', assignments),
+ table.write('latest', latest),
+ ]);
+ }
+ // At this point, either the state has been loaded successfully, or fresh state
+ // with a new copy of the manifest has been produced. At this point, the `Driver`
+ // can have its internals hydrated from the state.
+ // Initialize the `versions` map by setting each hash to a new `AppVersion` instance
+ // for that manifest.
+ Object.keys(manifests).forEach((hash) => {
+ const manifest = manifests[hash];
+ // If the manifest is newly initialized, an AppVersion may have already been
+ // created for it.
+ if (!this.versions.has(hash)) {
+ this.versions.set(hash, new AppVersion(this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash));
+ }
+ });
+ // Map each client ID to its associated hash. Along the way, verify that the hash
+ // is still valid for that client ID. It should not be possible for a client to
+ // still be associated with a hash that was since removed from the state.
+ Object.keys(assignments).forEach((clientId) => {
+ const hash = assignments[clientId];
+ if (this.versions.has(hash)) {
+ this.clientVersionMap.set(clientId, hash);
+ }
+ else {
+ this.clientVersionMap.set(clientId, latest.latest);
+ this.debugger.log(`Unknown version ${hash} mapped for client ${clientId}, using latest instead`, `initialize: map assignments`);
+ }
+ });
+ // Set the latest version.
+ this.latestHash = latest.latest;
+ // Finally, assert that the latest version is in fact loaded.
+ if (!this.versions.has(latest.latest)) {
+ throw new Error(`Invariant violated (initialize): latest hash ${latest.latest} has no known manifest`);
+ }
+ // Finally, wait for the scheduling of initialization of all versions in the
+ // manifest. Ordinarily this just schedules the initializations to happen during
+ // the next idle period, but in development mode this might actually wait for the
+ // full initialization.
+ // If any of these initializations fail, versionFailed() will be called either
+ // synchronously or asynchronously to handle the failure and re-map clients.
+ yield Promise.all(Object.keys(manifests).map((hash) => __awaiter(this, void 0, void 0, function* () {
+ try {
+ // Attempt to schedule or initialize this version. If this operation is
+ // successful, then initialization either succeeded or was scheduled. If
+ // it fails, then full initialization was attempted and failed.
+ yield this.scheduleInitialization(this.versions.get(hash));
+ }
+ catch (err) {
+ this.debugger.log(err, `initialize: schedule init of ${hash}`);
+ return false;
+ }
+ })));
+ });
+ }
+ lookupVersionByHash(hash, debugName = 'lookupVersionByHash') {
+ // The version should exist, but check just in case.
+ if (!this.versions.has(hash)) {
+ throw new Error(`Invariant violated (${debugName}): want AppVersion for ${hash} but not loaded`);
+ }
+ return this.versions.get(hash);
+ }
+ /**
+ * Decide which version of the manifest to use for the event.
+ */
+ assignVersion(event) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // First, check whether the event has a (non empty) client ID. If it does, the version may
+ // already be associated.
+ const clientId = event.clientId;
+ if (clientId) {
+ // Check if there is an assigned client id.
+ if (this.clientVersionMap.has(clientId)) {
+ // There is an assignment for this client already.
+ const hash = this.clientVersionMap.get(clientId);
+ let appVersion = this.lookupVersionByHash(hash, 'assignVersion');
+ // Ordinarily, this client would be served from its assigned version. But, if this
+ // request is a navigation request, this client can be updated to the latest
+ // version immediately.
+ if (this.state === DriverReadyState.NORMAL && hash !== this.latestHash &&
+ appVersion.isNavigationRequest(event.request)) {
+ // Update this client to the latest version immediately.
+ if (this.latestHash === null) {
+ throw new Error(`Invariant violated (assignVersion): latestHash was null`);
+ }
+ const client = yield this.scope.clients.get(clientId);
+ yield this.updateClient(client);
+ appVersion = this.lookupVersionByHash(this.latestHash, 'assignVersion');
+ }
+ // TODO: make sure the version is valid.
+ return appVersion;
+ }
+ else {
+ // This is the first time this client ID has been seen. Whether the SW is in a
+ // state to handle new clients depends on the current readiness state, so check
+ // that first.
+ if (this.state !== DriverReadyState.NORMAL) {
+ // It's not safe to serve new clients in the current state. It's possible that
+ // this is an existing client which has not been mapped yet (see below) but
+ // even if that is the case, it's invalid to make an assignment to a known
+ // invalid version, even if that assignment was previously implicit. Return
+ // undefined here to let the caller know that no assignment is possible at
+ // this time.
+ return null;
+ }
+ // It's safe to handle this request. Two cases apply. Either:
+ // 1) the browser assigned a client ID at the time of the navigation request, and
+ // this is truly the first time seeing this client, or
+ // 2) a navigation request came previously from the same client, but with no client
+ // ID attached. Browsers do this to avoid creating a client under the origin in
+ // the event the navigation request is just redirected.
+ //
+ // In case 1, the latest version can safely be used.
+ // In case 2, the latest version can be used, with the assumption that the previous
+ // navigation request was answered under the same version. This assumption relies
+ // on the fact that it's unlikely an update will come in between the navigation
+ // request and requests for subsequent resources on that page.
+ // First validate the current state.
+ if (this.latestHash === null) {
+ throw new Error(`Invariant violated (assignVersion): latestHash was null`);
+ }
+ // Pin this client ID to the current latest version, indefinitely.
+ this.clientVersionMap.set(clientId, this.latestHash);
+ yield this.sync();
+ // Return the latest `AppVersion`.
+ return this.lookupVersionByHash(this.latestHash, 'assignVersion');
+ }
+ }
+ else {
+ // No client ID was associated with the request. This must be a navigation request
+ // for a new client. First check that the SW is accepting new clients.
+ if (this.state !== DriverReadyState.NORMAL) {
+ return null;
+ }
+ // Serve it with the latest version, and assume that the client will actually get
+ // associated with that version on the next request.
+ // First validate the current state.
+ if (this.latestHash === null) {
+ throw new Error(`Invariant violated (assignVersion): latestHash was null`);
+ }
+ // Return the latest `AppVersion`.
+ return this.lookupVersionByHash(this.latestHash, 'assignVersion');
+ }
+ });
+ }
+ fetchLatestManifest(ignoreOfflineError = false) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const res = yield this.safeFetch(this.adapter.newRequest('ngsw.json?ngsw-cache-bust=' + Math.random()));
+ if (!res.ok) {
+ if (res.status === 404) {
+ yield this.deleteAllCaches();
+ yield this.scope.registration.unregister();
+ }
+ else if ((res.status === 503 || res.status === 504) && ignoreOfflineError) {
+ return null;
+ }
+ throw new Error(`Manifest fetch failed! (status: ${res.status})`);
+ }
+ this.lastUpdateCheck = this.adapter.time;
+ return res.json();
+ });
+ }
+ deleteAllCaches() {
+ return __awaiter(this, void 0, void 0, function* () {
+ const cacheNames = yield this.scope.caches.keys();
+ const ownCacheNames = cacheNames.filter(name => name.startsWith(`${this.adapter.cacheNamePrefix}:`));
+ yield Promise.all(ownCacheNames.map(name => this.scope.caches.delete(name)));
+ });
+ }
+ /**
+ * Schedule the SW's attempt to reach a fully prefetched state for the given AppVersion
+ * when the SW is not busy and has connectivity. This returns a Promise which must be
+ * awaited, as under some conditions the AppVersion might be initialized immediately.
+ */
+ scheduleInitialization(appVersion) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const initialize = () => __awaiter(this, void 0, void 0, function* () {
+ try {
+ yield appVersion.initializeFully();
+ }
+ catch (err) {
+ this.debugger.log(err, `initializeFully for ${appVersion.manifestHash}`);
+ yield this.versionFailed(appVersion, err);
+ }
+ });
+ // TODO: better logic for detecting localhost.
+ if (this.scope.registration.scope.indexOf('://localhost') > -1) {
+ return initialize();
+ }
+ this.idle.schedule(`initialization(${appVersion.manifestHash})`, initialize);
+ });
+ }
+ versionFailed(appVersion, err) {
+ return __awaiter(this, void 0, void 0, function* () {
+ // This particular AppVersion is broken. First, find the manifest hash.
+ const broken = Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
+ if (broken === undefined) {
+ // This version is no longer in use anyway, so nobody cares.
+ return;
+ }
+ const brokenHash = broken[0];
+ const affectedClients = Array.from(this.clientVersionMap.entries())
+ .filter(([clientId, hash]) => hash === brokenHash)
+ .map(([clientId]) => clientId);
+ // TODO: notify affected apps.
+ // The action taken depends on whether the broken manifest is the active (latest) or not.
+ // If so, the SW cannot accept new clients, but can continue to service old ones.
+ if (this.latestHash === brokenHash) {
+ // The latest manifest is broken. This means that new clients are at the mercy of the
+ // network, but caches continue to be valid for previous versions. This is
+ // unfortunate but unavoidable.
+ this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
+ this.stateMessage = `Degraded due to: ${errorToString(err)}`;
+ // Cancel the binding for the affected clients.
+ affectedClients.forEach(clientId => this.clientVersionMap.delete(clientId));
+ }
+ else {
+ // The latest version is viable, but this older version isn't. The only
+ // possible remedy is to stop serving the older version and go to the network.
+ // Put the affected clients on the latest version.
+ affectedClients.forEach(clientId => this.clientVersionMap.set(clientId, this.latestHash));
+ }
+ try {
+ yield this.sync();
+ }
+ catch (err2) {
+ // We are already in a bad state. No need to make things worse.
+ // Just log the error and move on.
+ this.debugger.log(err2, `Driver.versionFailed(${err.message || err})`);
+ }
+ });
+ }
+ setupUpdate(manifest, hash) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const newVersion = new AppVersion(this.scope, this.adapter, this.db, this.idle, this.debugger, manifest, hash);
+ // Firstly, check if the manifest version is correct.
+ if (manifest.configVersion !== SUPPORTED_CONFIG_VERSION) {
+ yield this.deleteAllCaches();
+ yield this.scope.registration.unregister();
+ throw new Error(`Invalid config version: expected ${SUPPORTED_CONFIG_VERSION}, got ${manifest.configVersion}.`);
+ }
+ // Cause the new version to become fully initialized. If this fails, then the
+ // version will not be available for use.
+ yield newVersion.initializeFully(this);
+ // Install this as an active version of the app.
+ this.versions.set(hash, newVersion);
+ // Future new clients will use this hash as the latest version.
+ this.latestHash = hash;
+ // If we are in `EXISTING_CLIENTS_ONLY` mode (meaning we didn't have a clean copy of the last
+ // latest version), we can now recover to `NORMAL` mode and start accepting new clients.
+ if (this.state === DriverReadyState.EXISTING_CLIENTS_ONLY) {
+ this.state = DriverReadyState.NORMAL;
+ this.stateMessage = '(nominal)';
+ }
+ yield this.sync();
+ yield this.notifyClientsAboutUpdate(newVersion);
+ });
+ }
+ checkForUpdate() {
+ return __awaiter(this, void 0, void 0, function* () {
+ let hash = '(unknown)';
+ try {
+ const manifest = yield this.fetchLatestManifest(true);
+ if (manifest === null) {
+ // Client or server offline. Unable to check for updates at this time.
+ // Continue to service clients (existing and new).
+ this.debugger.log('Check for update aborted. (Client or server offline.)');
+ return false;
+ }
+ hash = hashManifest(manifest);
+ // Check whether this is really an update.
+ if (this.versions.has(hash)) {
+ return false;
+ }
+ yield this.setupUpdate(manifest, hash);
+ return true;
+ }
+ catch (err) {
+ this.debugger.log(err, `Error occurred while updating to manifest ${hash}`);
+ this.state = DriverReadyState.EXISTING_CLIENTS_ONLY;
+ this.stateMessage = `Degraded due to failed initialization: ${errorToString(err)}`;
+ return false;
+ }
+ });
+ }
+ /**
+ * Synchronize the existing state to the underlying database.
+ */
+ sync() {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Open up the DB table.
+ const table = yield this.db.open('control');
+ // Construct a serializable map of hashes to manifests.
+ const manifests = {};
+ this.versions.forEach((version, hash) => {
+ manifests[hash] = version.manifest;
+ });
+ // Construct a serializable map of client ids to version hashes.
+ const assignments = {};
+ this.clientVersionMap.forEach((hash, clientId) => {
+ assignments[clientId] = hash;
+ });
+ // Record the latest entry. Since this is a sync which is necessarily happening after
+ // initialization, latestHash should always be valid.
+ const latest = {
+ latest: this.latestHash,
+ };
+ // Synchronize all of these.
+ yield Promise.all([
+ table.write('manifests', manifests),
+ table.write('assignments', assignments),
+ table.write('latest', latest),
+ ]);
+ });
+ }
+ cleanupCaches() {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Query for all currently active clients, and list the client ids. This may skip
+ // some clients in the browser back-forward cache, but not much can be done about
+ // that.
+ const activeClients = (yield this.scope.clients.matchAll()).map(client => client.id);
+ // A simple list of client ids that the SW has kept track of. Subtracting
+ // activeClients from this list will result in the set of client ids which are
+ // being tracked but are no longer used in the browser, and thus can be cleaned up.
+ const knownClients = Array.from(this.clientVersionMap.keys());
+ // Remove clients in the clientVersionMap that are no longer active.
+ knownClients.filter(id => activeClients.indexOf(id) === -1)
+ .forEach(id => this.clientVersionMap.delete(id));
+ // Next, determine the set of versions which are still used. All others can be
+ // removed.
+ const usedVersions = new Set();
+ this.clientVersionMap.forEach((version, _) => usedVersions.add(version));
+ // Collect all obsolete versions by filtering out used versions from the set of all versions.
+ const obsoleteVersions = Array.from(this.versions.keys())
+ .filter(version => !usedVersions.has(version) && version !== this.latestHash);
+ // Remove all the versions which are no longer used.
+ yield obsoleteVersions.reduce((previous, version) => __awaiter(this, void 0, void 0, function* () {
+ // Wait for the other cleanup operations to complete.
+ yield previous;
+ // Try to get past the failure of one particular version to clean up (this
+ // shouldn't happen, but handle it just in case).
+ try {
+ // Get ahold of the AppVersion for this particular hash.
+ const instance = this.versions.get(version);
+ // Delete it from the canonical map.
+ this.versions.delete(version);
+ // Clean it up.
+ yield instance.cleanup();
+ }
+ catch (err) {
+ // Oh well? Not much that can be done here. These caches will be removed when
+ // the SW revs its format version, which happens from time to time.
+ this.debugger.log(err, `cleanupCaches - cleanup ${version}`);
+ }
+ }), Promise.resolve());
+ // Commit all the changes to the saved state.
+ yield this.sync();
+ });
+ }
+ /**
+ * Delete caches that were used by older versions of `@angular/service-worker` to avoid running
+ * into storage quota limitations imposed by browsers.
+ * (Since at this point the SW has claimed all clients, it is safe to remove those caches.)
+ */
+ cleanupOldSwCaches() {
+ return __awaiter(this, void 0, void 0, function* () {
+ const cacheNames = yield this.scope.caches.keys();
+ const oldSwCacheNames = cacheNames.filter(name => /^ngsw:(?!\/)/.test(name));
+ yield Promise.all(oldSwCacheNames.map(name => this.scope.caches.delete(name)));
+ });
+ }
+ /**
+ * Determine if a specific version of the given resource is cached anywhere within the SW,
+ * and fetch it if so.
+ */
+ lookupResourceWithHash(url, hash) {
+ return Array
+ // Scan through the set of all cached versions, valid or otherwise. It's safe to do such
+ // lookups even for invalid versions as the cached version of a resource will have the
+ // same hash regardless.
+ .from(this.versions.values())
+ // Reduce the set of versions to a single potential result. At any point along the
+ // reduction, if a response has already been identified, then pass it through, as no
+ // future operation could change the response. If no response has been found yet, keep
+ // checking versions until one is or until all versions have been exhausted.
+ .reduce((prev, version) => __awaiter(this, void 0, void 0, function* () {
+ // First, check the previous result. If a non-null result has been found already, just
+ // return it.
+ if ((yield prev) !== null) {
+ return prev;
+ }
+ // No result has been found yet. Try the next `AppVersion`.
+ return version.lookupResourceWithHash(url, hash);
+ }), Promise.resolve(null));
+ }
+ lookupResourceWithoutHash(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.initialized;
+ const version = this.versions.get(this.latestHash);
+ return version ? version.lookupResourceWithoutHash(url) : null;
+ });
+ }
+ previouslyCachedResources() {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.initialized;
+ const version = this.versions.get(this.latestHash);
+ return version ? version.previouslyCachedResources() : [];
+ });
+ }
+ recentCacheStatus(url) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const version = this.versions.get(this.latestHash);
+ return version ? version.recentCacheStatus(url) : UpdateCacheStatus.NOT_CACHED;
+ });
+ }
+ mergeHashWithAppData(manifest, hash) {
+ return {
+ hash,
+ appData: manifest.appData,
+ };
+ }
+ notifyClientsAboutUnrecoverableState(appVersion, reason) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const broken = Array.from(this.versions.entries()).find(([hash, version]) => version === appVersion);
+ if (broken === undefined) {
+ // This version is no longer in use anyway, so nobody cares.
+ return;
+ }
+ const brokenHash = broken[0];
+ const affectedClients = Array.from(this.clientVersionMap.entries())
+ .filter(([clientId, hash]) => hash === brokenHash)
+ .map(([clientId]) => clientId);
+ yield Promise.all(affectedClients.map((clientId) => __awaiter(this, void 0, void 0, function* () {
+ const client = yield this.scope.clients.get(clientId);
+ client.postMessage({ type: 'UNRECOVERABLE_STATE', reason });
+ })));
+ });
+ }
+ notifyClientsAboutUpdate(next) {
+ return __awaiter(this, void 0, void 0, function* () {
+ yield this.initialized;
+ const clients = yield this.scope.clients.matchAll();
+ yield Promise.all(clients.map((client) => __awaiter(this, void 0, void 0, function* () {
+ // Firstly, determine which version this client is on.
+ const version = this.clientVersionMap.get(client.id);
+ if (version === undefined) {
+ // Unmapped client - assume it's the latest.
+ return;
+ }
+ if (version === this.latestHash) {
+ // Client is already on the latest version, no need for a notification.
+ return;
+ }
+ const current = this.versions.get(version);
+ // Send a notice.
+ const notice = {
+ type: 'UPDATE_AVAILABLE',
+ current: this.mergeHashWithAppData(current.manifest, version),
+ available: this.mergeHashWithAppData(next.manifest, this.latestHash),
+ };
+ client.postMessage(notice);
+ })));
+ });
+ }
+ broadcast(msg) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const clients = yield this.scope.clients.matchAll();
+ clients.forEach(client => {
+ client.postMessage(msg);
+ });
+ });
+ }
+ debugState() {
+ return __awaiter(this, void 0, void 0, function* () {
+ return {
+ state: DriverReadyState[this.state],
+ why: this.stateMessage,
+ latestHash: this.latestHash,
+ lastUpdateCheck: this.lastUpdateCheck,
+ };
+ });
+ }
+ debugVersions() {
+ return __awaiter(this, void 0, void 0, function* () {
+ // Build list of versions.
+ return Array.from(this.versions.keys()).map(hash => {
+ const version = this.versions.get(hash);
+ const clients = Array.from(this.clientVersionMap.entries())
+ .filter(([clientId, version]) => version === hash)
+ .map(([clientId, version]) => clientId);
+ return {
+ hash,
+ manifest: version.manifest,
+ clients,
+ status: '',
+ };
+ });
+ });
+ }
+ debugIdleState() {
+ return __awaiter(this, void 0, void 0, function* () {
+ return {
+ queue: this.idle.taskDescriptions,
+ lastTrigger: this.idle.lastTrigger,
+ lastRun: this.idle.lastRun,
+ };
+ });
+ }
+ safeFetch(req) {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ return yield this.scope.fetch(req);
+ }
+ catch (err) {
+ this.debugger.log(err, `Driver.fetch(${req.url})`);
+ return this.adapter.newResponse(null, {
+ status: 504,
+ statusText: 'Gateway Timeout',
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+ const scope = self;
+ const adapter = new Adapter(scope.registration.scope);
+ const driver = new Driver(scope, adapter, new CacheDatabase(scope, adapter));
+
+}());
diff --git a/www/ngsw.json b/www/ngsw.json
new file mode 100644
index 00000000000..f3dcc22e77e
--- /dev/null
+++ b/www/ngsw.json
@@ -0,0 +1,93 @@
+{
+ "configVersion": 1,
+ "timestamp": 1616997038070,
+ "index": "/index.html",
+ "assetGroups": [
+ {
+ "name": "app",
+ "installMode": "prefetch",
+ "updateMode": "prefetch",
+ "cacheQueryOptions": {
+ "ignoreVary": true
+ },
+ "urls": [
+ "/favicon.ico",
+ "/index.html",
+ "/main.4f0c125d57b6a4294ef7.js",
+ "/manifest.webmanifest",
+ "/polyfills.35a5ca1855eb057f016a.js",
+ "/runtime.acf0dec4155e77772545.js",
+ "/styles.8516d628d171b69b64ae.css"
+ ],
+ "patterns": []
+ },
+ {
+ "name": "assets",
+ "installMode": "lazy",
+ "updateMode": "prefetch",
+ "cacheQueryOptions": {
+ "ignoreVary": true
+ },
+ "urls": [
+ "/MaterialIcons-Regular.4674f8ded773cb03e824.eot",
+ "/MaterialIcons-Regular.5e7382c63da0098d634a.ttf",
+ "/MaterialIcons-Regular.83bebaf37c09c7e1c3ee.woff",
+ "/MaterialIcons-Regular.cff684e59ffb052d72cb.woff2",
+ "/assets/icons/icon-128x128.png",
+ "/assets/icons/icon-144x144.png",
+ "/assets/icons/icon-152x152.png",
+ "/assets/icons/icon-192x192.png",
+ "/assets/icons/icon-384x384.png",
+ "/assets/icons/icon-512x512.png",
+ "/assets/icons/icon-72x72.png",
+ "/assets/icons/icon-96x96.png",
+ "/assets/logo-dark.svg",
+ "/assets/logo-light.svg"
+ ],
+ "patterns": []
+ }
+ ],
+ "dataGroups": [],
+ "hashTable": {
+ "/MaterialIcons-Regular.4674f8ded773cb03e824.eot": "26fb8cecb5512223277b4d290a24492a0f09ede1",
+ "/MaterialIcons-Regular.5e7382c63da0098d634a.ttf": "fc05de31234e0090f7ddc28ce1b23af4026cb1da",
+ "/MaterialIcons-Regular.83bebaf37c09c7e1c3ee.woff": "c6c953c2ccb2ca9abb21db8dbf473b5a435f0082",
+ "/MaterialIcons-Regular.cff684e59ffb052d72cb.woff2": "09963592e8c953cc7e14e3fb0a5b05d5042e8435",
+ "/assets/icons/icon-128x128.png": "dae3b6ed49bdaf4327b92531d4b5b4a5d30c7532",
+ "/assets/icons/icon-144x144.png": "b0bd89982e08f9bd2b642928f5391915b74799a7",
+ "/assets/icons/icon-152x152.png": "7479a9477815dfd9668d60f8b3b2fba709b91310",
+ "/assets/icons/icon-192x192.png": "1abd80d431a237a853ce38147d8c63752f10933b",
+ "/assets/icons/icon-384x384.png": "329749cd6393768d3131ed6304c136b1ca05f2fd",
+ "/assets/icons/icon-512x512.png": "559d9c4318b45a1f2b10596bbb4c960fe521dbcc",
+ "/assets/icons/icon-72x72.png": "c457e56089a36952cd67156f9996bc4ce54a5ed9",
+ "/assets/icons/icon-96x96.png": "3914125a4b445bf111c5627875fc190f560daa41",
+ "/assets/logo-dark.svg": "3c5dbbd620a781a0d1e3680cb697982dd44a1613",
+ "/assets/logo-light.svg": "29908fafefe60c105f06b2f8d69f7c1eed436c17",
+ "/favicon.ico": "22f6a4a3bcaafafb0254e0f2fa4ceb89e505e8b2",
+ "/index.html": "441529d1440c800bcf9133ddfb2b383bf66a18f6",
+ "/main.4f0c125d57b6a4294ef7.js": "a8a5975961a64fa46b02dc5b5ad49273b0d6a9fd",
+ "/manifest.webmanifest": "e3c62aa5d0d6a32fa90da1196d0928f85a7b2470",
+ "/polyfills.35a5ca1855eb057f016a.js": "578360d24ce37383f261c7ef63b1f822d28e8dde",
+ "/runtime.acf0dec4155e77772545.js": "a9aafcf49f49145093fc831efd9b8e2f6c71bb9c",
+ "/styles.8516d628d171b69b64ae.css": "8db7eea8822571c4e74ee64622e7eec67213d139"
+ },
+ "navigationUrls": [
+ {
+ "positive": true,
+ "regex": "^\\/.*$"
+ },
+ {
+ "positive": false,
+ "regex": "^\\/(?:.+\\/)?[^/]*\\.[^/]*$"
+ },
+ {
+ "positive": false,
+ "regex": "^\\/(?:.+\\/)?[^/]*__[^/]*$"
+ },
+ {
+ "positive": false,
+ "regex": "^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$"
+ }
+ ],
+ "navigationRequestStrategy": "performance"
+}
\ No newline at end of file
diff --git a/www/polyfills.35a5ca1855eb057f016a.js b/www/polyfills.35a5ca1855eb057f016a.js
new file mode 100644
index 00000000000..60f4a02a2fd
--- /dev/null
+++ b/www/polyfills.35a5ca1855eb057f016a.js
@@ -0,0 +1 @@
+(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{1:function(e,t,n){e.exports=n("hN/g")},"hN/g":function(e,t,n){"use strict";n.r(t),n("pDpN")},pDpN:function(e,t,n){var o,r;void 0===(r="function"==typeof(o=function(){"use strict";!function(e){const t=e.performance;function n(e){t&&t.mark&&t.mark(e)}function o(e,n){t&&t.measure&&t.measure(e,n)}n("Zone");const r=e.__Zone_symbol_prefix||"__zone_symbol__";function s(e){return r+e}const i=!0===e[s("forceDuplicateZoneCheck")];if(e.Zone){if(i||"function"!=typeof e.Zone.__symbol__)throw new Error("Zone already loaded.");return e.Zone}class a{constructor(e,t){this._parent=e,this._name=t?t.name||"unnamed":"",this._properties=t&&t.properties||{},this._zoneDelegate=new l(this,this._parent&&this._parent._zoneDelegate,t)}static assertZonePatched(){if(e.Promise!==C.ZoneAwarePromise)throw new Error("Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten.\nMost likely cause is that a Promise polyfill has been loaded after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. If you must load one, do so before loading zone.js.)")}static get root(){let e=a.current;for(;e.parent;)e=e.parent;return e}static get current(){return z.zone}static get currentTask(){return j}static __load_patch(t,r){if(C.hasOwnProperty(t)){if(i)throw Error("Already loaded patch: "+t)}else if(!e["__Zone_disable_"+t]){const s="Zone:"+t;n(s),C[t]=r(e,a,O),o(s,s)}}get parent(){return this._parent}get name(){return this._name}get(e){const t=this.getZoneWith(e);if(t)return t._properties[e]}getZoneWith(e){let t=this;for(;t;){if(t._properties.hasOwnProperty(e))return t;t=t._parent}return null}fork(e){if(!e)throw new Error("ZoneSpec required!");return this._zoneDelegate.fork(this,e)}wrap(e,t){if("function"!=typeof e)throw new Error("Expecting function got: "+e);const n=this._zoneDelegate.intercept(this,e,t),o=this;return function(){return o.runGuarded(n,this,arguments,t)}}run(e,t,n,o){z={parent:z,zone:this};try{return this._zoneDelegate.invoke(this,e,t,n,o)}finally{z=z.parent}}runGuarded(e,t=null,n,o){z={parent:z,zone:this};try{try{return this._zoneDelegate.invoke(this,e,t,n,o)}catch(r){if(this._zoneDelegate.handleError(this,r))throw r}}finally{z=z.parent}}runTask(e,t,n){if(e.zone!=this)throw new Error("A task can only be run in the zone of creation! (Creation: "+(e.zone||y).name+"; Execution: "+this.name+")");if(e.state===v&&(e.type===P||e.type===D))return;const o=e.state!=E;o&&e._transitionTo(E,T),e.runCount++;const r=j;j=e,z={parent:z,zone:this};try{e.type==D&&e.data&&!e.data.isPeriodic&&(e.cancelFn=void 0);try{return this._zoneDelegate.invokeTask(this,e,t,n)}catch(s){if(this._zoneDelegate.handleError(this,s))throw s}}finally{e.state!==v&&e.state!==Z&&(e.type==P||e.data&&e.data.isPeriodic?o&&e._transitionTo(T,E):(e.runCount=0,this._updateTaskCount(e,-1),o&&e._transitionTo(v,E,v))),z=z.parent,j=r}}scheduleTask(e){if(e.zone&&e.zone!==this){let t=this;for(;t;){if(t===e.zone)throw Error(`can not reschedule task to ${this.name} which is descendants of the original zone ${e.zone.name}`);t=t.parent}}e._transitionTo(b,v);const t=[];e._zoneDelegates=t,e._zone=this;try{e=this._zoneDelegate.scheduleTask(this,e)}catch(n){throw e._transitionTo(Z,b,v),this._zoneDelegate.handleError(this,n),n}return e._zoneDelegates===t&&this._updateTaskCount(e,1),e.state==b&&e._transitionTo(T,b),e}scheduleMicroTask(e,t,n,o){return this.scheduleTask(new u(S,e,t,n,o,void 0))}scheduleMacroTask(e,t,n,o,r){return this.scheduleTask(new u(D,e,t,n,o,r))}scheduleEventTask(e,t,n,o,r){return this.scheduleTask(new u(P,e,t,n,o,r))}cancelTask(e){if(e.zone!=this)throw new Error("A task can only be cancelled in the zone of creation! (Creation: "+(e.zone||y).name+"; Execution: "+this.name+")");e._transitionTo(w,T,E);try{this._zoneDelegate.cancelTask(this,e)}catch(t){throw e._transitionTo(Z,w),this._zoneDelegate.handleError(this,t),t}return this._updateTaskCount(e,-1),e._transitionTo(v,w),e.runCount=0,e}_updateTaskCount(e,t){const n=e._zoneDelegates;-1==t&&(e._zoneDelegates=null);for(let o=0;oe.hasTask(n,o),onScheduleTask:(e,t,n,o)=>e.scheduleTask(n,o),onInvokeTask:(e,t,n,o,r,s)=>e.invokeTask(n,o,r,s),onCancelTask:(e,t,n,o)=>e.cancelTask(n,o)};class l{constructor(e,t,n){this._taskCounts={microTask:0,macroTask:0,eventTask:0},this.zone=e,this._parentDelegate=t,this._forkZS=n&&(n&&n.onFork?n:t._forkZS),this._forkDlgt=n&&(n.onFork?t:t._forkDlgt),this._forkCurrZone=n&&(n.onFork?this.zone:t._forkCurrZone),this._interceptZS=n&&(n.onIntercept?n:t._interceptZS),this._interceptDlgt=n&&(n.onIntercept?t:t._interceptDlgt),this._interceptCurrZone=n&&(n.onIntercept?this.zone:t._interceptCurrZone),this._invokeZS=n&&(n.onInvoke?n:t._invokeZS),this._invokeDlgt=n&&(n.onInvoke?t:t._invokeDlgt),this._invokeCurrZone=n&&(n.onInvoke?this.zone:t._invokeCurrZone),this._handleErrorZS=n&&(n.onHandleError?n:t._handleErrorZS),this._handleErrorDlgt=n&&(n.onHandleError?t:t._handleErrorDlgt),this._handleErrorCurrZone=n&&(n.onHandleError?this.zone:t._handleErrorCurrZone),this._scheduleTaskZS=n&&(n.onScheduleTask?n:t._scheduleTaskZS),this._scheduleTaskDlgt=n&&(n.onScheduleTask?t:t._scheduleTaskDlgt),this._scheduleTaskCurrZone=n&&(n.onScheduleTask?this.zone:t._scheduleTaskCurrZone),this._invokeTaskZS=n&&(n.onInvokeTask?n:t._invokeTaskZS),this._invokeTaskDlgt=n&&(n.onInvokeTask?t:t._invokeTaskDlgt),this._invokeTaskCurrZone=n&&(n.onInvokeTask?this.zone:t._invokeTaskCurrZone),this._cancelTaskZS=n&&(n.onCancelTask?n:t._cancelTaskZS),this._cancelTaskDlgt=n&&(n.onCancelTask?t:t._cancelTaskDlgt),this._cancelTaskCurrZone=n&&(n.onCancelTask?this.zone:t._cancelTaskCurrZone),this._hasTaskZS=null,this._hasTaskDlgt=null,this._hasTaskDlgtOwner=null,this._hasTaskCurrZone=null;const o=n&&n.onHasTask;(o||t&&t._hasTaskZS)&&(this._hasTaskZS=o?n:c,this._hasTaskDlgt=t,this._hasTaskDlgtOwner=this,this._hasTaskCurrZone=e,n.onScheduleTask||(this._scheduleTaskZS=c,this._scheduleTaskDlgt=t,this._scheduleTaskCurrZone=this.zone),n.onInvokeTask||(this._invokeTaskZS=c,this._invokeTaskDlgt=t,this._invokeTaskCurrZone=this.zone),n.onCancelTask||(this._cancelTaskZS=c,this._cancelTaskDlgt=t,this._cancelTaskCurrZone=this.zone))}fork(e,t){return this._forkZS?this._forkZS.onFork(this._forkDlgt,this.zone,e,t):new a(e,t)}intercept(e,t,n){return this._interceptZS?this._interceptZS.onIntercept(this._interceptDlgt,this._interceptCurrZone,e,t,n):t}invoke(e,t,n,o,r){return this._invokeZS?this._invokeZS.onInvoke(this._invokeDlgt,this._invokeCurrZone,e,t,n,o,r):t.apply(n,o)}handleError(e,t){return!this._handleErrorZS||this._handleErrorZS.onHandleError(this._handleErrorDlgt,this._handleErrorCurrZone,e,t)}scheduleTask(e,t){let n=t;if(this._scheduleTaskZS)this._hasTaskZS&&n._zoneDelegates.push(this._hasTaskDlgtOwner),n=this._scheduleTaskZS.onScheduleTask(this._scheduleTaskDlgt,this._scheduleTaskCurrZone,e,t),n||(n=t);else if(t.scheduleFn)t.scheduleFn(t);else{if(t.type!=S)throw new Error("Task is missing scheduleFn.");k(t)}return n}invokeTask(e,t,n,o){return this._invokeTaskZS?this._invokeTaskZS.onInvokeTask(this._invokeTaskDlgt,this._invokeTaskCurrZone,e,t,n,o):t.callback.apply(n,o)}cancelTask(e,t){let n;if(this._cancelTaskZS)n=this._cancelTaskZS.onCancelTask(this._cancelTaskDlgt,this._cancelTaskCurrZone,e,t);else{if(!t.cancelFn)throw Error("Task is not cancelable");n=t.cancelFn(t)}return n}hasTask(e,t){try{this._hasTaskZS&&this._hasTaskZS.onHasTask(this._hasTaskDlgt,this._hasTaskCurrZone,e,t)}catch(n){this.handleError(e,n)}}_updateTaskCount(e,t){const n=this._taskCounts,o=n[e],r=n[e]=o+t;if(r<0)throw new Error("More tasks executed then were scheduled.");0!=o&&0!=r||this.hasTask(this.zone,{microTask:n.microTask>0,macroTask:n.macroTask>0,eventTask:n.eventTask>0,change:e})}}class u{constructor(t,n,o,r,s,i){if(this._zone=null,this.runCount=0,this._zoneDelegates=null,this._state="notScheduled",this.type=t,this.source=n,this.data=r,this.scheduleFn=s,this.cancelFn=i,!o)throw new Error("callback is not defined");this.callback=o;const a=this;this.invoke=t===P&&r&&r.useG?u.invokeTask:function(){return u.invokeTask.call(e,a,this,arguments)}}static invokeTask(e,t,n){e||(e=this),I++;try{return e.runCount++,e.zone.runTask(e,t,n)}finally{1==I&&m(),I--}}get zone(){return this._zone}get state(){return this._state}cancelScheduleRequest(){this._transitionTo(v,b)}_transitionTo(e,t,n){if(this._state!==t&&this._state!==n)throw new Error(`${this.type} '${this.source}': can not transition to '${e}', expecting state '${t}'${n?" or '"+n+"'":""}, was '${this._state}'.`);this._state=e,e==v&&(this._zoneDelegates=null)}toString(){return this.data&&void 0!==this.data.handleId?this.data.handleId.toString():Object.prototype.toString.call(this)}toJSON(){return{type:this.type,state:this.state,source:this.source,zone:this.zone.name,runCount:this.runCount}}}const h=s("setTimeout"),p=s("Promise"),f=s("then");let d,g=[],_=!1;function k(t){if(0===I&&0===g.length)if(d||e[p]&&(d=e[p].resolve(0)),d){let e=d[f];e||(e=d.then),e.call(d,m)}else e[h](m,0);t&&g.push(t)}function m(){if(!_){for(_=!0;g.length;){const t=g;g=[];for(let n=0;nz,onUnhandledError:N,microtaskDrainDone:N,scheduleMicroTask:k,showUncaughtError:()=>!a[s("ignoreConsoleErrorUncaughtError")],patchEventTarget:()=>[],patchOnProperties:N,patchMethod:()=>N,bindArguments:()=>[],patchThen:()=>N,patchMacroTask:()=>N,setNativePromise:e=>{e&&"function"==typeof e.resolve&&(d=e.resolve(0))},patchEventPrototype:()=>N,isIEOrEdge:()=>!1,getGlobalObjects:()=>{},ObjectDefineProperty:()=>N,ObjectGetOwnPropertyDescriptor:()=>{},ObjectCreate:()=>{},ArraySlice:()=>[],patchClass:()=>N,wrapWithCurrentZone:()=>N,filterProperties:()=>[],attachOriginToPatched:()=>N,_redefineProperty:()=>N,patchCallbacks:()=>N};let z={parent:null,zone:new a(null,null)},j=null,I=0;function N(){}o("Zone","Zone"),e.Zone=a}("undefined"!=typeof window&&window||"undefined"!=typeof self&&self||global),Zone.__load_patch("ZoneAwarePromise",(e,t,n)=>{const o=Object.getOwnPropertyDescriptor,r=Object.defineProperty,s=n.symbol,i=[],a=!0===e[s("DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION")],c=s("Promise"),l=s("then");n.onUnhandledError=e=>{if(n.showUncaughtError()){const t=e&&e.rejection;t?console.error("Unhandled Promise rejection:",t instanceof Error?t.message:t,"; Zone:",e.zone.name,"; Task:",e.task&&e.task.source,"; Value:",t,t instanceof Error?t.stack:void 0):console.error(e)}},n.microtaskDrainDone=()=>{for(;i.length;){const t=i.shift();try{t.zone.runGuarded(()=>{throw t})}catch(e){h(e)}}};const u=s("unhandledPromiseRejectionHandler");function h(e){n.onUnhandledError(e);try{const n=t[u];"function"==typeof n&&n.call(this,e)}catch(o){}}function p(e){return e&&e.then}function f(e){return e}function d(e){return O.reject(e)}const g=s("state"),_=s("value"),k=s("finally"),m=s("parentPromiseValue"),y=s("parentPromiseState"),v=null,b=!0,T=!1;function E(e,t){return n=>{try{Z(e,t,n)}catch(o){Z(e,!1,o)}}}const w=s("currentTaskTrace");function Z(e,o,s){const c=function(){let e=!1;return function(t){return function(){e||(e=!0,t.apply(null,arguments))}}}();if(e===s)throw new TypeError("Promise resolved with itself");if(e[g]===v){let h=null;try{"object"!=typeof s&&"function"!=typeof s||(h=s&&s.then)}catch(u){return c(()=>{Z(e,!1,u)})(),e}if(o!==T&&s instanceof O&&s.hasOwnProperty(g)&&s.hasOwnProperty(_)&&s[g]!==v)D(s),Z(e,s[g],s[_]);else if(o!==T&&"function"==typeof h)try{h.call(s,c(E(e,o)),c(E(e,!1)))}catch(u){c(()=>{Z(e,!1,u)})()}else{e[g]=o;const c=e[_];if(e[_]=s,e[k]===k&&o===b&&(e[g]=e[y],e[_]=e[m]),o===T&&s instanceof Error){const e=t.currentTask&&t.currentTask.data&&t.currentTask.data.__creationTrace__;e&&r(s,w,{configurable:!0,enumerable:!1,writable:!0,value:e})}for(let t=0;t{try{const o=e[_],r=!!n&&k===n[k];r&&(n[m]=o,n[y]=s);const a=t.run(i,void 0,r&&i!==d&&i!==f?[]:[o]);Z(n,!0,a)}catch(o){Z(n,!1,o)}},n)}const C=function(){};class O{static toString(){return"function ZoneAwarePromise() { [native code] }"}static resolve(e){return Z(new this(null),b,e)}static reject(e){return Z(new this(null),T,e)}static race(e){let t,n,o=new this((e,o)=>{t=e,n=o});function r(e){t(e)}function s(e){n(e)}for(let i of e)p(i)||(i=this.resolve(i)),i.then(r,s);return o}static all(e){return O.allWithCallback(e)}static allSettled(e){return(this&&this.prototype instanceof O?this:O).allWithCallback(e,{thenCallback:e=>({status:"fulfilled",value:e}),errorCallback:e=>({status:"rejected",reason:e})})}static allWithCallback(e,t){let n,o,r=new this((e,t)=>{n=e,o=t}),s=2,i=0;const a=[];for(let l of e){p(l)||(l=this.resolve(l));const e=i;try{l.then(o=>{a[e]=t?t.thenCallback(o):o,s--,0===s&&n(a)},r=>{t?(a[e]=t.errorCallback(r),s--,0===s&&n(a)):o(r)})}catch(c){o(c)}s++,i++}return s-=2,0===s&&n(a),r}constructor(e){const t=this;if(!(t instanceof O))throw new Error("Must be an instanceof Promise.");t[g]=v,t[_]=[];try{e&&e(E(t,b),E(t,T))}catch(n){Z(t,!1,n)}}get[Symbol.toStringTag](){return"Promise"}get[Symbol.species](){return O}then(e,n){let o=this.constructor[Symbol.species];o&&"function"==typeof o||(o=this.constructor||O);const r=new o(C),s=t.current;return this[g]==v?this[_].push(s,r,e,n):P(this,s,r,e,n),r}catch(e){return this.then(null,e)}finally(e){let n=this.constructor[Symbol.species];n&&"function"==typeof n||(n=O);const o=new n(C);o[k]=k;const r=t.current;return this[g]==v?this[_].push(r,o,e,e):P(this,r,o,e,e),o}}O.resolve=O.resolve,O.reject=O.reject,O.race=O.race,O.all=O.all;const z=e[c]=e.Promise,j=t.__symbol__("ZoneAwarePromise");let I=o(e,"Promise");I&&!I.configurable||(I&&delete I.writable,I&&delete I.value,I||(I={configurable:!0,enumerable:!0}),I.get=function(){return e[j]?e[j]:e[c]},I.set=function(t){t===O?e[j]=t:(e[c]=t,t.prototype[l]||R(t),n.setNativePromise(t))},r(e,"Promise",I)),e.Promise=O;const N=s("thenPatched");function R(e){const t=e.prototype,n=o(t,"then");if(n&&(!1===n.writable||!n.configurable))return;const r=t.then;t[l]=r,e.prototype.then=function(e,t){return new O((e,t)=>{r.call(this,e,t)}).then(e,t)},e[N]=!0}if(n.patchThen=R,z){R(z);const t=e.fetch;"function"==typeof t&&(e[n.symbol("fetch")]=t,e.fetch=(x=t,function(){let e=x.apply(this,arguments);if(e instanceof O)return e;let t=e.constructor;return t[N]||R(t),e}))}var x;return Promise[t.__symbol__("uncaughtPromiseErrors")]=i,O});const e=Object.getOwnPropertyDescriptor,t=Object.defineProperty,n=Object.getPrototypeOf,o=Object.create,r=Array.prototype.slice,s="addEventListener",i="removeEventListener",a=Zone.__symbol__(s),c=Zone.__symbol__(i),l="true",u="false",h=Zone.__symbol__("");function p(e,t){return Zone.current.wrap(e,t)}function f(e,t,n,o,r){return Zone.current.scheduleMacroTask(e,t,n,o,r)}const d=Zone.__symbol__,g="undefined"!=typeof window,_=g?window:void 0,k=g&&_||"object"==typeof self&&self||global,m=[null];function y(e,t){for(let n=e.length-1;n>=0;n--)"function"==typeof e[n]&&(e[n]=p(e[n],t+"_"+n));return e}function v(e){return!e||!1!==e.writable&&!("function"==typeof e.get&&void 0===e.set)}const b="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope,T=!("nw"in k)&&void 0!==k.process&&"[object process]"==={}.toString.call(k.process),E=!T&&!b&&!(!g||!_.HTMLElement),w=void 0!==k.process&&"[object process]"==={}.toString.call(k.process)&&!b&&!(!g||!_.HTMLElement),Z={},S=function(e){if(!(e=e||k.event))return;let t=Z[e.type];t||(t=Z[e.type]=d("ON_PROPERTY"+e.type));const n=this||e.target||k,o=n[t];let r;if(E&&n===_&&"error"===e.type){const t=e;r=o&&o.call(this,t.message,t.filename,t.lineno,t.colno,t.error),!0===r&&e.preventDefault()}else r=o&&o.apply(this,arguments),null==r||r||e.preventDefault();return r};function D(n,o,r){let s=e(n,o);if(!s&&r&&e(r,o)&&(s={enumerable:!0,configurable:!0}),!s||!s.configurable)return;const i=d("on"+o+"patched");if(n.hasOwnProperty(i)&&n[i])return;delete s.writable,delete s.value;const a=s.get,c=s.set,l=o.substr(2);let u=Z[l];u||(u=Z[l]=d("ON_PROPERTY"+l)),s.set=function(e){let t=this;t||n!==k||(t=k),t&&(t[u]&&t.removeEventListener(l,S),c&&c.apply(t,m),"function"==typeof e?(t[u]=e,t.addEventListener(l,S,!1)):t[u]=null)},s.get=function(){let e=this;if(e||n!==k||(e=k),!e)return null;const t=e[u];if(t)return t;if(a){let t=a&&a.call(this);if(t)return s.set.call(this,t),"function"==typeof e.removeAttribute&&e.removeAttribute(o),t}return null},t(n,o,s),n[i]=!0}function P(e,t,n){if(t)for(let o=0;ofunction(t,o){const s=n(t,o);return s.cbIdx>=0&&"function"==typeof o[s.cbIdx]?f(s.name,o[s.cbIdx],s,r):e.apply(t,o)})}function I(e,t){e[d("OriginalDelegate")]=t}let N=!1,R=!1;function x(){try{const e=_.navigator.userAgent;if(-1!==e.indexOf("MSIE ")||-1!==e.indexOf("Trident/"))return!0}catch(e){}return!1}function M(){if(N)return R;N=!0;try{const e=_.navigator.userAgent;-1===e.indexOf("MSIE ")&&-1===e.indexOf("Trident/")&&-1===e.indexOf("Edge/")||(R=!0)}catch(e){}return R}Zone.__load_patch("toString",e=>{const t=Function.prototype.toString,n=d("OriginalDelegate"),o=d("Promise"),r=d("Error"),s=function(){if("function"==typeof this){const s=this[n];if(s)return"function"==typeof s?t.call(s):Object.prototype.toString.call(s);if(this===Promise){const n=e[o];if(n)return t.call(n)}if(this===Error){const n=e[r];if(n)return t.call(n)}}return t.call(this)};s[n]=t,Function.prototype.toString=s;const i=Object.prototype.toString;Object.prototype.toString=function(){return this instanceof Promise?"[object Promise]":i.call(this)}});let L=!1;if("undefined"!=typeof window)try{const e=Object.defineProperty({},"passive",{get:function(){L=!0}});window.addEventListener("test",e,e),window.removeEventListener("test",e,e)}catch(he){L=!1}const A={useG:!0},H={},F={},G=new RegExp("^"+h+"(\\w+)(true|false)$"),B=d("propagationStopped");function q(e,t){const n=(t?t(e):e)+u,o=(t?t(e):e)+l,r=h+n,s=h+o;H[e]={},H[e].false=r,H[e].true=s}function W(e,t,o){const r=o&&o.add||s,a=o&&o.rm||i,c=o&&o.listeners||"eventListeners",p=o&&o.rmAll||"removeAllListeners",f=d(r),g="."+r+":",_=function(e,t,n){if(e.isRemoved)return;const o=e.callback;"object"==typeof o&&o.handleEvent&&(e.callback=e=>o.handleEvent(e),e.originalDelegate=o),e.invoke(e,t,[n]);const r=e.options;r&&"object"==typeof r&&r.once&&t[a].call(t,n.type,e.originalDelegate?e.originalDelegate:e.callback,r)},k=function(t){if(!(t=t||e.event))return;const n=this||t.target||e,o=n[H[t.type].false];if(o)if(1===o.length)_(o[0],n,t);else{const e=o.slice();for(let o=0;ofunction(t,n){t[B]=!0,e&&e.apply(t,n)})}function $(e,t,n,o,r){const s=Zone.__symbol__(o);if(t[s])return;const i=t[s]=t[o];t[o]=function(s,a,c){return a&&a.prototype&&r.forEach((function(t){const r=`${n}.${o}::`+t,s=a.prototype;if(s.hasOwnProperty(t)){const n=e.ObjectGetOwnPropertyDescriptor(s,t);n&&n.value?(n.value=e.wrapWithCurrentZone(n.value,r),e._redefineProperty(a.prototype,t,n)):s[t]&&(s[t]=e.wrapWithCurrentZone(s[t],r))}else s[t]&&(s[t]=e.wrapWithCurrentZone(s[t],r))})),i.call(t,s,a,c)},e.attachOriginToPatched(t[o],i)}const X=["absolutedeviceorientation","afterinput","afterprint","appinstalled","beforeinstallprompt","beforeprint","beforeunload","devicelight","devicemotion","deviceorientation","deviceorientationabsolute","deviceproximity","hashchange","languagechange","message","mozbeforepaint","offline","online","paint","pageshow","pagehide","popstate","rejectionhandled","storage","unhandledrejection","unload","userproximity","vrdisplayconnected","vrdisplaydisconnected","vrdisplaypresentchange"],J=["encrypted","waitingforkey","msneedkey","mozinterruptbegin","mozinterruptend"],Y=["load"],K=["blur","error","focus","load","resize","scroll","messageerror"],Q=["bounce","finish","start"],ee=["loadstart","progress","abort","error","load","progress","timeout","loadend","readystatechange"],te=["upgradeneeded","complete","abort","success","error","blocked","versionchange","close"],ne=["close","error","open","message"],oe=["error","message"],re=["abort","animationcancel","animationend","animationiteration","auxclick","beforeinput","blur","cancel","canplay","canplaythrough","change","compositionstart","compositionupdate","compositionend","cuechange","click","close","contextmenu","curechange","dblclick","drag","dragend","dragenter","dragexit","dragleave","dragover","drop","durationchange","emptied","ended","error","focus","focusin","focusout","gotpointercapture","input","invalid","keydown","keypress","keyup","load","loadstart","loadeddata","loadedmetadata","lostpointercapture","mousedown","mouseenter","mouseleave","mousemove","mouseout","mouseover","mouseup","mousewheel","orientationchange","pause","play","playing","pointercancel","pointerdown","pointerenter","pointerleave","pointerlockchange","mozpointerlockchange","webkitpointerlockerchange","pointerlockerror","mozpointerlockerror","webkitpointerlockerror","pointermove","pointout","pointerover","pointerup","progress","ratechange","reset","resize","scroll","seeked","seeking","select","selectionchange","selectstart","show","sort","stalled","submit","suspend","timeupdate","volumechange","touchcancel","touchmove","touchstart","touchend","transitioncancel","transitionend","waiting","wheel"].concat(["webglcontextrestored","webglcontextlost","webglcontextcreationerror"],["autocomplete","autocompleteerror"],["toggle"],["afterscriptexecute","beforescriptexecute","DOMContentLoaded","freeze","fullscreenchange","mozfullscreenchange","webkitfullscreenchange","msfullscreenchange","fullscreenerror","mozfullscreenerror","webkitfullscreenerror","msfullscreenerror","readystatechange","visibilitychange","resume"],X,["beforecopy","beforecut","beforepaste","copy","cut","paste","dragstart","loadend","animationstart","search","transitionrun","transitionstart","webkitanimationend","webkitanimationiteration","webkitanimationstart","webkittransitionend"],["activate","afterupdate","ariarequest","beforeactivate","beforedeactivate","beforeeditfocus","beforeupdate","cellchange","controlselect","dataavailable","datasetchanged","datasetcomplete","errorupdate","filterchange","layoutcomplete","losecapture","move","moveend","movestart","propertychange","resizeend","resizestart","rowenter","rowexit","rowsdelete","rowsinserted","command","compassneedscalibration","deactivate","help","mscontentzoom","msmanipulationstatechanged","msgesturechange","msgesturedoubletap","msgestureend","msgesturehold","msgesturestart","msgesturetap","msgotpointercapture","msinertiastart","mslostpointercapture","mspointercancel","mspointerdown","mspointerenter","mspointerhover","mspointerleave","mspointermove","mspointerout","mspointerover","mspointerup","pointerout","mssitemodejumplistitemremoved","msthumbnailclick","stop","storagecommit"]);function se(e,t,n){if(!n||0===n.length)return t;const o=n.filter(t=>t.target===e);if(!o||0===o.length)return t;const r=o[0].ignoreProperties;return t.filter(e=>-1===r.indexOf(e))}function ie(e,t,n,o){e&&P(e,se(e,t,n),o)}function ae(e,t){if(T&&!w)return;if(Zone[e.symbol("patchEvents")])return;const o="undefined"!=typeof WebSocket,r=t.__Zone_ignore_on_properties;if(E){const e=window,t=x?[{target:e,ignoreProperties:["error"]}]:[];ie(e,re.concat(["messageerror"]),r?r.concat(t):r,n(e)),ie(Document.prototype,re,r),void 0!==e.SVGElement&&ie(e.SVGElement.prototype,re,r),ie(Element.prototype,re,r),ie(HTMLElement.prototype,re,r),ie(HTMLMediaElement.prototype,J,r),ie(HTMLFrameSetElement.prototype,X.concat(K),r),ie(HTMLBodyElement.prototype,X.concat(K),r),ie(HTMLFrameElement.prototype,Y,r),ie(HTMLIFrameElement.prototype,Y,r);const o=e.HTMLMarqueeElement;o&&ie(o.prototype,Q,r);const s=e.Worker;s&&ie(s.prototype,oe,r)}const s=t.XMLHttpRequest;s&&ie(s.prototype,ee,r);const i=t.XMLHttpRequestEventTarget;i&&ie(i&&i.prototype,ee,r),"undefined"!=typeof IDBIndex&&(ie(IDBIndex.prototype,te,r),ie(IDBRequest.prototype,te,r),ie(IDBOpenDBRequest.prototype,te,r),ie(IDBDatabase.prototype,te,r),ie(IDBTransaction.prototype,te,r),ie(IDBCursor.prototype,te,r)),o&&ie(WebSocket.prototype,ne,r)}Zone.__load_patch("util",(n,a,c)=>{c.patchOnProperties=P,c.patchMethod=z,c.bindArguments=y,c.patchMacroTask=j;const f=a.__symbol__("BLACK_LISTED_EVENTS"),d=a.__symbol__("UNPATCHED_EVENTS");n[d]&&(n[f]=n[d]),n[f]&&(a[f]=a[d]=n[f]),c.patchEventPrototype=V,c.patchEventTarget=W,c.isIEOrEdge=M,c.ObjectDefineProperty=t,c.ObjectGetOwnPropertyDescriptor=e,c.ObjectCreate=o,c.ArraySlice=r,c.patchClass=O,c.wrapWithCurrentZone=p,c.filterProperties=se,c.attachOriginToPatched=I,c._redefineProperty=Object.defineProperty,c.patchCallbacks=$,c.getGlobalObjects=()=>({globalSources:F,zoneSymbolEventNames:H,eventNames:re,isBrowser:E,isMix:w,isNode:T,TRUE_STR:l,FALSE_STR:u,ZONE_SYMBOL_PREFIX:h,ADD_EVENT_LISTENER_STR:s,REMOVE_EVENT_LISTENER_STR:i})});const ce=d("zoneTask");function le(e,t,n,o){let r=null,s=null;n+=o;const i={};function a(t){const n=t.data;return n.args[0]=function(){try{t.invoke.apply(this,arguments)}finally{t.data&&t.data.isPeriodic||("number"==typeof n.handleId?delete i[n.handleId]:n.handleId&&(n.handleId[ce]=null))}},n.handleId=r.apply(e,n.args),t}function c(e){return s(e.data.handleId)}r=z(e,t+=o,n=>function(r,s){if("function"==typeof s[0]){const e=f(t,s[0],{isPeriodic:"Interval"===o,delay:"Timeout"===o||"Interval"===o?s[1]||0:void 0,args:s},a,c);if(!e)return e;const n=e.data.handleId;return"number"==typeof n?i[n]=e:n&&(n[ce]=e),n&&n.ref&&n.unref&&"function"==typeof n.ref&&"function"==typeof n.unref&&(e.ref=n.ref.bind(n),e.unref=n.unref.bind(n)),"number"==typeof n||n?n:e}return n.apply(e,s)}),s=z(e,n,t=>function(n,o){const r=o[0];let s;"number"==typeof r?s=i[r]:(s=r&&r[ce],s||(s=r)),s&&"string"==typeof s.type?"notScheduled"!==s.state&&(s.cancelFn&&s.data.isPeriodic||0===s.runCount)&&("number"==typeof r?delete i[r]:r&&(r[ce]=null),s.zone.cancelTask(s)):t.apply(e,o)})}function ue(e,t){if(Zone[t.symbol("patchEventTarget")])return;const{eventNames:n,zoneSymbolEventNames:o,TRUE_STR:r,FALSE_STR:s,ZONE_SYMBOL_PREFIX:i}=t.getGlobalObjects();for(let c=0;c{const t=e[Zone.__symbol__("legacyPatch")];t&&t()}),Zone.__load_patch("timers",e=>{const t="set",n="clear";le(e,t,n,"Timeout"),le(e,t,n,"Interval"),le(e,t,n,"Immediate")}),Zone.__load_patch("requestAnimationFrame",e=>{le(e,"request","cancel","AnimationFrame"),le(e,"mozRequest","mozCancel","AnimationFrame"),le(e,"webkitRequest","webkitCancel","AnimationFrame")}),Zone.__load_patch("blocking",(e,t)=>{const n=["alert","prompt","confirm"];for(let o=0;ofunction(o,s){return t.current.run(n,e,s,r)})}),Zone.__load_patch("EventTarget",(e,t,n)=>{(function(e,t){t.patchEventPrototype(e,t)})(e,n),ue(e,n);const o=e.XMLHttpRequestEventTarget;o&&o.prototype&&n.patchEventTarget(e,[o.prototype]),O("MutationObserver"),O("WebKitMutationObserver"),O("IntersectionObserver"),O("FileReader")}),Zone.__load_patch("on_property",(e,t,n)=>{ae(n,e)}),Zone.__load_patch("customElements",(e,t,n)=>{!function(e,t){const{isBrowser:n,isMix:o}=t.getGlobalObjects();(n||o)&&e.customElements&&"customElements"in e&&t.patchCallbacks(t,e.customElements,"customElements","define",["connectedCallback","disconnectedCallback","adoptedCallback","attributeChangedCallback"])}(e,n)}),Zone.__load_patch("XHR",(e,t)=>{!function(e){const u=e.XMLHttpRequest;if(!u)return;const h=u.prototype;let p=h[a],g=h[c];if(!p){const t=e.XMLHttpRequestEventTarget;if(t){const e=t.prototype;p=e[a],g=e[c]}}const _="readystatechange",k="scheduled";function m(e){const o=e.data,i=o.target;i[s]=!1,i[l]=!1;const u=i[r];p||(p=i[a],g=i[c]),u&&g.call(i,_,u);const h=i[r]=()=>{if(i.readyState===i.DONE)if(!o.aborted&&i[s]&&e.state===k){const n=i[t.__symbol__("loadfalse")];if(n&&n.length>0){const r=e.invoke;e.invoke=function(){const n=i[t.__symbol__("loadfalse")];for(let t=0;tfunction(e,t){return e[o]=0==t[2],e[i]=t[1],b.apply(e,t)}),T=d("fetchTaskAborting"),E=d("fetchTaskScheduling"),w=z(h,"send",()=>function(e,n){if(!0===t.current[E])return w.apply(e,n);if(e[o])return w.apply(e,n);{const t={target:e,url:e[i],isPeriodic:!1,args:n,aborted:!1},o=f("XMLHttpRequest.send",y,t,m,v);e&&!0===e[l]&&!t.aborted&&o.state===k&&o.invoke()}}),Z=z(h,"abort",()=>function(e,o){const r=e[n];if(r&&"string"==typeof r.type){if(null==r.cancelFn||r.data&&r.data.aborted)return;r.zone.cancelTask(r)}else if(!0===t.current[T])return Z.apply(e,o)})}(e);const n=d("xhrTask"),o=d("xhrSync"),r=d("xhrListener"),s=d("xhrScheduled"),i=d("xhrURL"),l=d("xhrErrorBeforeScheduled")}),Zone.__load_patch("geolocation",t=>{t.navigator&&t.navigator.geolocation&&function(t,n){const o=t.constructor.name;for(let r=0;r{const t=function(){return e.apply(this,y(arguments,o+"."+s))};return I(t,e),t})(i)}}}(t.navigator.geolocation,["getCurrentPosition","watchPosition"])}),Zone.__load_patch("PromiseRejectionEvent",(e,t)=>{function n(t){return function(n){U(e,t).forEach(o=>{const r=e.PromiseRejectionEvent;if(r){const e=new r(t,{promise:n.promise,reason:n.rejection});o.invoke(e)}})}}e.PromiseRejectionEvent&&(t[d("unhandledPromiseRejectionHandler")]=n("unhandledrejection"),t[d("rejectionHandledHandler")]=n("rejectionhandled"))})})?o.call(t,n,t,e):o)||(e.exports=r)}},[[1,0]]]);
\ No newline at end of file
diff --git a/www/runtime.acf0dec4155e77772545.js b/www/runtime.acf0dec4155e77772545.js
new file mode 100644
index 00000000000..effa6aee759
--- /dev/null
+++ b/www/runtime.acf0dec4155e77772545.js
@@ -0,0 +1 @@
+!function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ event.waitUntil(self.clients.claim());
+ self.registration.unregister().then(() => {
+ console.log('NGSW Safety Worker - unregistered old service worker');
+ });
+});
diff --git a/www/styles.8516d628d171b69b64ae.css b/www/styles.8516d628d171b69b64ae.css
new file mode 100644
index 00000000000..16969cc7b6b
--- /dev/null
+++ b/www/styles.8516d628d171b69b64ae.css
@@ -0,0 +1,6 @@
+.xterm{font-feature-settings:"liga" 0;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm{cursor:text}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:.5}.xterm-underline{text-decoration:underline}@font-face{font-family:Material Icons;font-style:normal;font-weight:400;src:url(MaterialIcons-Regular.4674f8ded773cb03e824.eot);src:local("Material Icons"),local("MaterialIcons-Regular"),url(MaterialIcons-Regular.cff684e59ffb052d72cb.woff2) format("woff2"),url(MaterialIcons-Regular.83bebaf37c09c7e1c3ee.woff) format("woff"),url(MaterialIcons-Regular.5e7382c63da0098d634a.ttf) format("truetype")}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:24px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}.mat-badge-content{font-weight:600;font-size:12px;font-family:Roboto,Helvetica Neue,sans-serif}.mat-badge-small .mat-badge-content{font-size:9px}.mat-badge-large .mat-badge-content{font-size:24px}.mat-h1,.mat-headline,.mat-typography h1{font:400 24px/32px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal;margin:0 0 16px}.mat-h2,.mat-title,.mat-typography h2{font:500 20px/32px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal;margin:0 0 16px}.mat-h3,.mat-subheading-2,.mat-typography h3{font:400 16px/28px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal;margin:0 0 16px}.mat-h4,.mat-subheading-1,.mat-typography h4{font:400 15px/24px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal;margin:0 0 16px}.mat-h5,.mat-typography h5{font:400 calc(14px * .83)/20px Roboto,Helvetica Neue,sans-serif;margin:0 0 12px}.mat-h6,.mat-typography h6{font:400 calc(14px * .67)/20px Roboto,Helvetica Neue,sans-serif;margin:0 0 12px}.mat-body-2,.mat-body-strong{font:500 14px/24px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-body,.mat-body-1,.mat-typography{font:400 14px/20px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-body-1 p,.mat-body p,.mat-typography p{margin:0 0 12px}.mat-caption,.mat-small{font:400 12px/20px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-display-4,.mat-typography .mat-display-4{font:300 112px/112px Roboto,Helvetica Neue,sans-serif;letter-spacing:-.05em;margin:0 0 56px}.mat-display-3,.mat-typography .mat-display-3{font:400 56px/56px Roboto,Helvetica Neue,sans-serif;letter-spacing:-.02em;margin:0 0 64px}.mat-display-2,.mat-typography .mat-display-2{font:400 45px/48px Roboto,Helvetica Neue,sans-serif;letter-spacing:-.005em;margin:0 0 64px}.mat-display-1,.mat-typography .mat-display-1{font:400 34px/40px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal;margin:0 0 64px}.mat-bottom-sheet-container{font:400 14px/20px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-button,.mat-fab,.mat-flat-button,.mat-icon-button,.mat-mini-fab,.mat-raised-button,.mat-stroked-button{font-family:Roboto,Helvetica Neue,sans-serif;font-size:14px;font-weight:500}.mat-button-toggle,.mat-card{font-family:Roboto,Helvetica Neue,sans-serif}.mat-card-title{font-size:24px;font-weight:500}.mat-card-header .mat-card-title{font-size:20px}.mat-card-content,.mat-card-subtitle{font-size:14px}.mat-checkbox{font-family:Roboto,Helvetica Neue,sans-serif}.mat-checkbox-layout .mat-checkbox-label{line-height:24px}.mat-chip{font-size:14px;font-weight:500}.mat-chip .mat-chip-remove.mat-icon,.mat-chip .mat-chip-trailing-icon.mat-icon{font-size:18px}.mat-table{font-family:Roboto,Helvetica Neue,sans-serif}.mat-header-cell{font-size:12px;font-weight:500}.mat-cell,.mat-footer-cell{font-size:14px}.mat-calendar{font-family:Roboto,Helvetica Neue,sans-serif}.mat-calendar-body{font-size:13px}.mat-calendar-body-label,.mat-calendar-period-button{font-size:14px;font-weight:500}.mat-calendar-table-header th{font-size:11px;font-weight:400}.mat-dialog-title{font:500 20px/32px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-expansion-panel-header{font-family:Roboto,Helvetica Neue,sans-serif;font-size:15px;font-weight:400}.mat-expansion-panel-content{font:400 14px/20px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-form-field{font-size:inherit;font-weight:400;line-height:1.125;font-family:Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-form-field-wrapper{padding-bottom:1.34375em}.mat-form-field-prefix .mat-icon,.mat-form-field-suffix .mat-icon{font-size:150%;line-height:1.125}.mat-form-field-prefix .mat-icon-button,.mat-form-field-suffix .mat-icon-button{height:1.5em;width:1.5em}.mat-form-field-prefix .mat-icon-button .mat-icon,.mat-form-field-suffix .mat-icon-button .mat-icon{height:1.125em;line-height:1.125}.mat-form-field-infix{padding:.5em 0;border-top:.84375em solid transparent}.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label,.mat-form-field-can-float .mat-input-server:focus+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.34375em) scale(.75);width:133.3333333333%}.mat-form-field-can-float .mat-input-server[label]:not(:label-shown)+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.34374em) scale(.75);width:133.3333433333%}.mat-form-field-label-wrapper{top:-.84375em;padding-top:.84375em}.mat-form-field-label{top:1.34375em}.mat-form-field-underline{bottom:1.34375em}.mat-form-field-subscript-wrapper{font-size:75%;margin-top:.6666666667em;top:calc(100% - 1.7916666667em)}.mat-form-field-appearance-legacy .mat-form-field-wrapper{padding-bottom:1.25em}.mat-form-field-appearance-legacy .mat-form-field-infix{padding:.4375em 0}.mat-form-field-appearance-legacy.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label,.mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server:focus+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.28125em) scale(.75) perspective(100px) translateZ(.001px);-ms-transform:translateY(-1.28125em) scale(.75);width:133.3333333333%}.mat-form-field-appearance-legacy.mat-form-field-can-float .mat-form-field-autofill-control:-webkit-autofill+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.28125em) scale(.75) perspective(100px) translateZ(.00101px);-ms-transform:translateY(-1.28124em) scale(.75);width:133.3333433333%}.mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server[label]:not(:label-shown)+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.28125em) scale(.75) perspective(100px) translateZ(.00102px);-ms-transform:translateY(-1.28123em) scale(.75);width:133.3333533333%}.mat-form-field-appearance-legacy .mat-form-field-label{top:1.28125em}.mat-form-field-appearance-legacy .mat-form-field-underline{bottom:1.25em}.mat-form-field-appearance-legacy .mat-form-field-subscript-wrapper{margin-top:.5416666667em;top:calc(100% - 1.6666666667em)}@media print{.mat-form-field-appearance-legacy.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label,.mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server:focus+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.28122em) scale(.75)}.mat-form-field-appearance-legacy.mat-form-field-can-float .mat-form-field-autofill-control:-webkit-autofill+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.28121em) scale(.75)}.mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server[label]:not(:label-shown)+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.2812em) scale(.75)}}.mat-form-field-appearance-fill .mat-form-field-infix{padding:.25em 0 .75em}.mat-form-field-appearance-fill .mat-form-field-label{top:1.09375em;margin-top:-.5em}.mat-form-field-appearance-fill.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label,.mat-form-field-appearance-fill.mat-form-field-can-float .mat-input-server:focus+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-.59375em) scale(.75);width:133.3333333333%}.mat-form-field-appearance-fill.mat-form-field-can-float .mat-input-server[label]:not(:label-shown)+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-.59374em) scale(.75);width:133.3333433333%}.mat-form-field-appearance-outline .mat-form-field-infix{padding:1em 0}.mat-form-field-appearance-outline .mat-form-field-label{top:1.84375em;margin-top:-.25em}.mat-form-field-appearance-outline.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label,.mat-form-field-appearance-outline.mat-form-field-can-float .mat-input-server:focus+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.59375em) scale(.75);width:133.3333333333%}.mat-form-field-appearance-outline.mat-form-field-can-float .mat-input-server[label]:not(:label-shown)+.mat-form-field-label-wrapper .mat-form-field-label{transform:translateY(-1.59374em) scale(.75);width:133.3333433333%}.mat-grid-tile-footer,.mat-grid-tile-header{font-size:14px}.mat-grid-tile-footer .mat-line,.mat-grid-tile-header .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-grid-tile-footer .mat-line:nth-child(n+2),.mat-grid-tile-header .mat-line:nth-child(n+2){font-size:12px}input.mat-input-element{margin-top:-.0625em}.mat-menu-item{font-family:Roboto,Helvetica Neue,sans-serif;font-size:14px;font-weight:400}.mat-paginator,.mat-paginator-page-size .mat-select-trigger{font-family:Roboto,Helvetica Neue,sans-serif;font-size:12px}.mat-radio-button,.mat-select{font-family:Roboto,Helvetica Neue,sans-serif}.mat-select-trigger{height:1.125em}.mat-slide-toggle-content,.mat-slider-thumb-label-text{font-family:Roboto,Helvetica Neue,sans-serif}.mat-slider-thumb-label-text{font-size:12px;font-weight:500}.mat-stepper-horizontal,.mat-stepper-vertical{font-family:Roboto,Helvetica Neue,sans-serif}.mat-step-label{font-size:14px;font-weight:400}.mat-step-sub-label-error{font-weight:400}.mat-step-label-error{font-size:14px}.mat-step-label-selected{font-size:14px;font-weight:500}.mat-tab-group,.mat-tab-label,.mat-tab-link{font-family:Roboto,Helvetica Neue,sans-serif}.mat-tab-label,.mat-tab-link{font-size:14px;font-weight:500}.mat-toolbar,.mat-toolbar h1,.mat-toolbar h2,.mat-toolbar h3,.mat-toolbar h4,.mat-toolbar h5,.mat-toolbar h6{font:500 20px/32px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal;margin:0}.mat-tooltip{font-family:Roboto,Helvetica Neue,sans-serif;font-size:10px;padding-top:6px;padding-bottom:6px}.mat-tooltip-handset{font-size:14px;padding-top:8px;padding-bottom:8px}.mat-list-item,.mat-list-option{font-family:Roboto,Helvetica Neue,sans-serif}.mat-list-base .mat-list-item{font-size:16px}.mat-list-base .mat-list-item .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-list-base .mat-list-item .mat-line:nth-child(n+2){font-size:14px}.mat-list-base .mat-list-option{font-size:16px}.mat-list-base .mat-list-option .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-list-base .mat-list-option .mat-line:nth-child(n+2){font-size:14px}.mat-list-base .mat-subheader{font-family:Roboto,Helvetica Neue,sans-serif;font-size:14px;font-weight:500}.mat-list-base[dense] .mat-list-item{font-size:12px}.mat-list-base[dense] .mat-list-item .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-list-base[dense] .mat-list-item .mat-line:nth-child(n+2),.mat-list-base[dense] .mat-list-option{font-size:12px}.mat-list-base[dense] .mat-list-option .mat-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;box-sizing:border-box}.mat-list-base[dense] .mat-list-option .mat-line:nth-child(n+2){font-size:12px}.mat-list-base[dense] .mat-subheader{font-family:Roboto,Helvetica Neue,sans-serif;font-size:12px;font-weight:500}.mat-option{font-family:Roboto,Helvetica Neue,sans-serif;font-size:16px}.mat-optgroup-label{font:500 14px/24px Roboto,Helvetica Neue,sans-serif;letter-spacing:normal}.mat-simple-snackbar{font-family:Roboto,Helvetica Neue,sans-serif;font-size:14px}.mat-simple-snackbar-action{line-height:1;font-family:inherit;font-size:inherit;font-weight:500}.mat-tree{font-family:Roboto,Helvetica Neue,sans-serif}.mat-nested-tree-node,.mat-tree-node{font-weight:400;font-size:14px}.mat-ripple{overflow:hidden;position:relative}.mat-ripple:not(:empty){transform:translateZ(0)}.mat-ripple.mat-ripple-unbounded{overflow:visible}.mat-ripple-element{position:absolute;border-radius:50%;pointer-events:none;transition:opacity,transform 0ms cubic-bezier(0,0,.2,1);transform:scale(0)}.cdk-high-contrast-active .mat-ripple-element{display:none}.cdk-visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;outline:0;-webkit-appearance:none;-moz-appearance:none}.cdk-global-overlay-wrapper,.cdk-overlay-container{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper,.cdk-overlay-pane{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{pointer-events:auto;box-sizing:border-box;max-width:100%;max-height:100%}.cdk-overlay-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;z-index:1000;pointer-events:auto;-webkit-tap-highlight-color:transparent;transition:opacity .4s cubic-bezier(.25,.8,.25,1);opacity:0}.cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:1}.cdk-high-contrast-active .cdk-overlay-backdrop.cdk-overlay-backdrop-showing{opacity:.6}.cdk-overlay-dark-backdrop{background:rgba(0,0,0,.32)}.cdk-overlay-transparent-backdrop,.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing{opacity:0}.cdk-overlay-connected-position-bounding-box{position:absolute;z-index:1000;display:flex;flex-direction:column;min-width:1px;min-height:1px}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}@keyframes cdk-text-field-autofill-start{
+ /*!*/}@keyframes cdk-text-field-autofill-end{
+ /*!*/}.cdk-text-field-autofill-monitored:-webkit-autofill{animation:cdk-text-field-autofill-start 0s 1ms}.cdk-text-field-autofill-monitored:not(:-webkit-autofill){animation:cdk-text-field-autofill-end 0s 1ms}textarea.cdk-textarea-autosize{resize:none}textarea.cdk-textarea-autosize-measuring{padding:2px 0!important;box-sizing:initial!important;height:auto!important;overflow:hidden!important}textarea.cdk-textarea-autosize-measuring-firefox{padding:2px 0!important;box-sizing:initial!important;height:0!important}.mat-focus-indicator,.mat-mdc-focus-indicator{position:relative}.mat-ripple-element{background-color:rgba(0,0,0,.1)}.mat-option{color:rgba(0,0,0,.87)}.mat-option.mat-active,.mat-option.mat-selected:not(.mat-option-multiple):not(.mat-option-disabled),.mat-option:focus:not(.mat-option-disabled),.mat-option:hover:not(.mat-option-disabled){background:rgba(0,0,0,.04)}.mat-option.mat-active{color:rgba(0,0,0,.87)}.mat-option.mat-option-disabled{color:rgba(0,0,0,.38)}.mat-primary .mat-option.mat-selected:not(.mat-option-disabled){color:#3f51b5}.mat-accent .mat-option.mat-selected:not(.mat-option-disabled){color:#ff4081}.mat-warn .mat-option.mat-selected:not(.mat-option-disabled){color:#f44336}.mat-optgroup-label{color:rgba(0,0,0,.54)}.mat-optgroup-disabled .mat-optgroup-label{color:rgba(0,0,0,.38)}.mat-pseudo-checkbox{color:rgba(0,0,0,.54)}.mat-pseudo-checkbox:after{color:#fafafa}.mat-pseudo-checkbox-disabled{color:#b0b0b0}.mat-primary .mat-pseudo-checkbox-checked,.mat-primary .mat-pseudo-checkbox-indeterminate{background:#3f51b5}.mat-accent .mat-pseudo-checkbox-checked,.mat-accent .mat-pseudo-checkbox-indeterminate,.mat-pseudo-checkbox-checked,.mat-pseudo-checkbox-indeterminate{background:#ff4081}.mat-warn .mat-pseudo-checkbox-checked,.mat-warn .mat-pseudo-checkbox-indeterminate{background:#f44336}.mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled,.mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled{background:#b0b0b0}.mat-app-background{background-color:#fafafa;color:rgba(0,0,0,.87)}.mat-elevation-z0{box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-elevation-z1{box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12)}.mat-elevation-z2{box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}.mat-elevation-z3{box-shadow:0 3px 3px -2px rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 1px 8px 0 rgba(0,0,0,.12)}.mat-elevation-z4{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.mat-elevation-z5{box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12)}.mat-elevation-z6{box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12)}.mat-elevation-z7{box-shadow:0 4px 5px -2px rgba(0,0,0,.2),0 7px 10px 1px rgba(0,0,0,.14),0 2px 16px 1px rgba(0,0,0,.12)}.mat-elevation-z8{box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12)}.mat-elevation-z9{box-shadow:0 5px 6px -3px rgba(0,0,0,.2),0 9px 12px 1px rgba(0,0,0,.14),0 3px 16px 2px rgba(0,0,0,.12)}.mat-elevation-z10{box-shadow:0 6px 6px -3px rgba(0,0,0,.2),0 10px 14px 1px rgba(0,0,0,.14),0 4px 18px 3px rgba(0,0,0,.12)}.mat-elevation-z11{box-shadow:0 6px 7px -4px rgba(0,0,0,.2),0 11px 15px 1px rgba(0,0,0,.14),0 4px 20px 3px rgba(0,0,0,.12)}.mat-elevation-z12{box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 12px 17px 2px rgba(0,0,0,.14),0 5px 22px 4px rgba(0,0,0,.12)}.mat-elevation-z13{box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 13px 19px 2px rgba(0,0,0,.14),0 5px 24px 4px rgba(0,0,0,.12)}.mat-elevation-z14{box-shadow:0 7px 9px -4px rgba(0,0,0,.2),0 14px 21px 2px rgba(0,0,0,.14),0 5px 26px 4px rgba(0,0,0,.12)}.mat-elevation-z15{box-shadow:0 8px 9px -5px rgba(0,0,0,.2),0 15px 22px 2px rgba(0,0,0,.14),0 6px 28px 5px rgba(0,0,0,.12)}.mat-elevation-z16{box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12)}.mat-elevation-z17{box-shadow:0 8px 11px -5px rgba(0,0,0,.2),0 17px 26px 2px rgba(0,0,0,.14),0 6px 32px 5px rgba(0,0,0,.12)}.mat-elevation-z18{box-shadow:0 9px 11px -5px rgba(0,0,0,.2),0 18px 28px 2px rgba(0,0,0,.14),0 7px 34px 6px rgba(0,0,0,.12)}.mat-elevation-z19{box-shadow:0 9px 12px -6px rgba(0,0,0,.2),0 19px 29px 2px rgba(0,0,0,.14),0 7px 36px 6px rgba(0,0,0,.12)}.mat-elevation-z20{box-shadow:0 10px 13px -6px rgba(0,0,0,.2),0 20px 31px 3px rgba(0,0,0,.14),0 8px 38px 7px rgba(0,0,0,.12)}.mat-elevation-z21{box-shadow:0 10px 13px -6px rgba(0,0,0,.2),0 21px 33px 3px rgba(0,0,0,.14),0 8px 40px 7px rgba(0,0,0,.12)}.mat-elevation-z22{box-shadow:0 10px 14px -6px rgba(0,0,0,.2),0 22px 35px 3px rgba(0,0,0,.14),0 8px 42px 7px rgba(0,0,0,.12)}.mat-elevation-z23{box-shadow:0 11px 14px -7px rgba(0,0,0,.2),0 23px 36px 3px rgba(0,0,0,.14),0 9px 44px 8px rgba(0,0,0,.12)}.mat-elevation-z24{box-shadow:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12)}.mat-theme-loaded-marker{display:none}.mat-autocomplete-panel{background:#fff;color:rgba(0,0,0,.87)}.mat-autocomplete-panel:not([class*=mat-elevation-z]){box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.mat-autocomplete-panel .mat-option.mat-selected:not(.mat-active):not(:hover){background:#fff}.mat-autocomplete-panel .mat-option.mat-selected:not(.mat-active):not(:hover):not(.mat-option-disabled){color:rgba(0,0,0,.87)}.mat-badge-content{color:#fff;background:#3f51b5}.cdk-high-contrast-active .mat-badge-content{outline:1px solid;border-radius:0}.mat-badge-accent .mat-badge-content{background:#ff4081;color:#fff}.mat-badge-warn .mat-badge-content{color:#fff;background:#f44336}.mat-badge{position:relative}.mat-badge-hidden .mat-badge-content{display:none}.mat-badge-disabled .mat-badge-content{background:#b9b9b9;color:rgba(0,0,0,.38)}.mat-badge-content{position:absolute;text-align:center;display:inline-block;border-radius:50%;transition:transform .2s ease-in-out;transform:scale(.6);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;pointer-events:none}.mat-badge-content._mat-animation-noopable,.ng-animate-disabled .mat-badge-content{transition:none}.mat-badge-content.mat-badge-active{transform:none}.mat-badge-small .mat-badge-content{width:16px;height:16px;line-height:16px}.mat-badge-small.mat-badge-above .mat-badge-content{top:-8px}.mat-badge-small.mat-badge-below .mat-badge-content{bottom:-8px}.mat-badge-small.mat-badge-before .mat-badge-content{left:-16px}[dir=rtl] .mat-badge-small.mat-badge-before .mat-badge-content{left:auto;right:-16px}.mat-badge-small.mat-badge-after .mat-badge-content{right:-16px}[dir=rtl] .mat-badge-small.mat-badge-after .mat-badge-content{right:auto;left:-16px}.mat-badge-small.mat-badge-overlap.mat-badge-before .mat-badge-content{left:-8px}[dir=rtl] .mat-badge-small.mat-badge-overlap.mat-badge-before .mat-badge-content{left:auto;right:-8px}.mat-badge-small.mat-badge-overlap.mat-badge-after .mat-badge-content{right:-8px}[dir=rtl] .mat-badge-small.mat-badge-overlap.mat-badge-after .mat-badge-content{right:auto;left:-8px}.mat-badge-medium .mat-badge-content{width:22px;height:22px;line-height:22px}.mat-badge-medium.mat-badge-above .mat-badge-content{top:-11px}.mat-badge-medium.mat-badge-below .mat-badge-content{bottom:-11px}.mat-badge-medium.mat-badge-before .mat-badge-content{left:-22px}[dir=rtl] .mat-badge-medium.mat-badge-before .mat-badge-content{left:auto;right:-22px}.mat-badge-medium.mat-badge-after .mat-badge-content{right:-22px}[dir=rtl] .mat-badge-medium.mat-badge-after .mat-badge-content{right:auto;left:-22px}.mat-badge-medium.mat-badge-overlap.mat-badge-before .mat-badge-content{left:-11px}[dir=rtl] .mat-badge-medium.mat-badge-overlap.mat-badge-before .mat-badge-content{left:auto;right:-11px}.mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content{right:-11px}[dir=rtl] .mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content{right:auto;left:-11px}.mat-badge-large .mat-badge-content{width:28px;height:28px;line-height:28px}.mat-badge-large.mat-badge-above .mat-badge-content{top:-14px}.mat-badge-large.mat-badge-below .mat-badge-content{bottom:-14px}.mat-badge-large.mat-badge-before .mat-badge-content{left:-28px}[dir=rtl] .mat-badge-large.mat-badge-before .mat-badge-content{left:auto;right:-28px}.mat-badge-large.mat-badge-after .mat-badge-content{right:-28px}[dir=rtl] .mat-badge-large.mat-badge-after .mat-badge-content{right:auto;left:-28px}.mat-badge-large.mat-badge-overlap.mat-badge-before .mat-badge-content{left:-14px}[dir=rtl] .mat-badge-large.mat-badge-overlap.mat-badge-before .mat-badge-content{left:auto;right:-14px}.mat-badge-large.mat-badge-overlap.mat-badge-after .mat-badge-content{right:-14px}[dir=rtl] .mat-badge-large.mat-badge-overlap.mat-badge-after .mat-badge-content{right:auto;left:-14px}.mat-bottom-sheet-container{box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12);background:#fff;color:rgba(0,0,0,.87)}.mat-button,.mat-icon-button,.mat-stroked-button{color:inherit;background:transparent}.mat-button.mat-primary,.mat-icon-button.mat-primary,.mat-stroked-button.mat-primary{color:#3f51b5}.mat-button.mat-accent,.mat-icon-button.mat-accent,.mat-stroked-button.mat-accent{color:#ff4081}.mat-button.mat-warn,.mat-icon-button.mat-warn,.mat-stroked-button.mat-warn{color:#f44336}.mat-button.mat-accent.mat-button-disabled,.mat-button.mat-button-disabled.mat-button-disabled,.mat-button.mat-primary.mat-button-disabled,.mat-button.mat-warn.mat-button-disabled,.mat-icon-button.mat-accent.mat-button-disabled,.mat-icon-button.mat-button-disabled.mat-button-disabled,.mat-icon-button.mat-primary.mat-button-disabled,.mat-icon-button.mat-warn.mat-button-disabled,.mat-stroked-button.mat-accent.mat-button-disabled,.mat-stroked-button.mat-button-disabled.mat-button-disabled,.mat-stroked-button.mat-primary.mat-button-disabled,.mat-stroked-button.mat-warn.mat-button-disabled{color:rgba(0,0,0,.26)}.mat-button.mat-primary .mat-button-focus-overlay,.mat-icon-button.mat-primary .mat-button-focus-overlay,.mat-stroked-button.mat-primary .mat-button-focus-overlay{background-color:#3f51b5}.mat-button.mat-accent .mat-button-focus-overlay,.mat-icon-button.mat-accent .mat-button-focus-overlay,.mat-stroked-button.mat-accent .mat-button-focus-overlay{background-color:#ff4081}.mat-button.mat-warn .mat-button-focus-overlay,.mat-icon-button.mat-warn .mat-button-focus-overlay,.mat-stroked-button.mat-warn .mat-button-focus-overlay{background-color:#f44336}.mat-button.mat-button-disabled .mat-button-focus-overlay,.mat-icon-button.mat-button-disabled .mat-button-focus-overlay,.mat-stroked-button.mat-button-disabled .mat-button-focus-overlay{background-color:initial}.mat-button .mat-ripple-element,.mat-icon-button .mat-ripple-element,.mat-stroked-button .mat-ripple-element{opacity:.1;background-color:currentColor}.mat-button-focus-overlay{background:#000}.mat-stroked-button:not(.mat-button-disabled){border-color:rgba(0,0,0,.12)}.mat-fab,.mat-flat-button,.mat-mini-fab,.mat-raised-button{color:rgba(0,0,0,.87);background-color:#fff}.mat-fab.mat-accent,.mat-fab.mat-primary,.mat-fab.mat-warn,.mat-flat-button.mat-accent,.mat-flat-button.mat-primary,.mat-flat-button.mat-warn,.mat-mini-fab.mat-accent,.mat-mini-fab.mat-primary,.mat-mini-fab.mat-warn,.mat-raised-button.mat-accent,.mat-raised-button.mat-primary,.mat-raised-button.mat-warn{color:#fff}.mat-fab.mat-accent.mat-button-disabled,.mat-fab.mat-button-disabled.mat-button-disabled,.mat-fab.mat-primary.mat-button-disabled,.mat-fab.mat-warn.mat-button-disabled,.mat-flat-button.mat-accent.mat-button-disabled,.mat-flat-button.mat-button-disabled.mat-button-disabled,.mat-flat-button.mat-primary.mat-button-disabled,.mat-flat-button.mat-warn.mat-button-disabled,.mat-mini-fab.mat-accent.mat-button-disabled,.mat-mini-fab.mat-button-disabled.mat-button-disabled,.mat-mini-fab.mat-primary.mat-button-disabled,.mat-mini-fab.mat-warn.mat-button-disabled,.mat-raised-button.mat-accent.mat-button-disabled,.mat-raised-button.mat-button-disabled.mat-button-disabled,.mat-raised-button.mat-primary.mat-button-disabled,.mat-raised-button.mat-warn.mat-button-disabled{color:rgba(0,0,0,.26)}.mat-fab.mat-primary,.mat-flat-button.mat-primary,.mat-mini-fab.mat-primary,.mat-raised-button.mat-primary{background-color:#3f51b5}.mat-fab.mat-accent,.mat-flat-button.mat-accent,.mat-mini-fab.mat-accent,.mat-raised-button.mat-accent{background-color:#ff4081}.mat-fab.mat-warn,.mat-flat-button.mat-warn,.mat-mini-fab.mat-warn,.mat-raised-button.mat-warn{background-color:#f44336}.mat-fab.mat-accent.mat-button-disabled,.mat-fab.mat-button-disabled.mat-button-disabled,.mat-fab.mat-primary.mat-button-disabled,.mat-fab.mat-warn.mat-button-disabled,.mat-flat-button.mat-accent.mat-button-disabled,.mat-flat-button.mat-button-disabled.mat-button-disabled,.mat-flat-button.mat-primary.mat-button-disabled,.mat-flat-button.mat-warn.mat-button-disabled,.mat-mini-fab.mat-accent.mat-button-disabled,.mat-mini-fab.mat-button-disabled.mat-button-disabled,.mat-mini-fab.mat-primary.mat-button-disabled,.mat-mini-fab.mat-warn.mat-button-disabled,.mat-raised-button.mat-accent.mat-button-disabled,.mat-raised-button.mat-button-disabled.mat-button-disabled,.mat-raised-button.mat-primary.mat-button-disabled,.mat-raised-button.mat-warn.mat-button-disabled{background-color:rgba(0,0,0,.12)}.mat-fab.mat-accent .mat-ripple-element,.mat-fab.mat-primary .mat-ripple-element,.mat-fab.mat-warn .mat-ripple-element,.mat-flat-button.mat-accent .mat-ripple-element,.mat-flat-button.mat-primary .mat-ripple-element,.mat-flat-button.mat-warn .mat-ripple-element,.mat-mini-fab.mat-accent .mat-ripple-element,.mat-mini-fab.mat-primary .mat-ripple-element,.mat-mini-fab.mat-warn .mat-ripple-element,.mat-raised-button.mat-accent .mat-ripple-element,.mat-raised-button.mat-primary .mat-ripple-element,.mat-raised-button.mat-warn .mat-ripple-element{background-color:hsla(0,0%,100%,.1)}.mat-flat-button:not([class*=mat-elevation-z]),.mat-stroked-button:not([class*=mat-elevation-z]){box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-raised-button:not([class*=mat-elevation-z]){box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}.mat-raised-button:not(.mat-button-disabled):active:not([class*=mat-elevation-z]){box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12)}.mat-raised-button.mat-button-disabled:not([class*=mat-elevation-z]){box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-fab:not([class*=mat-elevation-z]),.mat-mini-fab:not([class*=mat-elevation-z]){box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12)}.mat-fab:not(.mat-button-disabled):active:not([class*=mat-elevation-z]),.mat-mini-fab:not(.mat-button-disabled):active:not([class*=mat-elevation-z]){box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 12px 17px 2px rgba(0,0,0,.14),0 5px 22px 4px rgba(0,0,0,.12)}.mat-fab.mat-button-disabled:not([class*=mat-elevation-z]),.mat-mini-fab.mat-button-disabled:not([class*=mat-elevation-z]){box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-button-toggle-group,.mat-button-toggle-standalone{box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}.mat-button-toggle-group-appearance-standard,.mat-button-toggle-standalone.mat-button-toggle-appearance-standard{box-shadow:none}.mat-button-toggle{color:rgba(0,0,0,.38)}.mat-button-toggle .mat-button-toggle-focus-overlay{background-color:rgba(0,0,0,.12)}.mat-button-toggle-appearance-standard{color:rgba(0,0,0,.87);background:#fff}.mat-button-toggle-appearance-standard .mat-button-toggle-focus-overlay{background-color:#000}.mat-button-toggle-group-appearance-standard .mat-button-toggle+.mat-button-toggle{border-left:1px solid rgba(0,0,0,.12)}[dir=rtl] .mat-button-toggle-group-appearance-standard .mat-button-toggle+.mat-button-toggle{border-left:none;border-right:1px solid rgba(0,0,0,.12)}.mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle+.mat-button-toggle{border-left:none;border-right:none;border-top:1px solid rgba(0,0,0,.12)}.mat-button-toggle-checked{background-color:#e0e0e0;color:rgba(0,0,0,.54)}.mat-button-toggle-checked.mat-button-toggle-appearance-standard{color:rgba(0,0,0,.87)}.mat-button-toggle-disabled{color:rgba(0,0,0,.26);background-color:#eee}.mat-button-toggle-disabled.mat-button-toggle-appearance-standard{background:#fff}.mat-button-toggle-disabled.mat-button-toggle-checked{background-color:#bdbdbd}.mat-button-toggle-group-appearance-standard,.mat-button-toggle-standalone.mat-button-toggle-appearance-standard{border:1px solid rgba(0,0,0,.12)}.mat-button-toggle-appearance-standard .mat-button-toggle-label-content{line-height:48px}.mat-card{background:#fff;color:rgba(0,0,0,.87)}.mat-card:not([class*=mat-elevation-z]){box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12)}.mat-card.mat-card-flat:not([class*=mat-elevation-z]){box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-card-subtitle{color:rgba(0,0,0,.54)}.mat-checkbox-frame{border-color:rgba(0,0,0,.54)}.mat-checkbox-checkmark{fill:#fafafa}.mat-checkbox-checkmark-path{stroke:#fafafa!important}.mat-checkbox-mixedmark{background-color:#fafafa}.mat-checkbox-checked.mat-primary .mat-checkbox-background,.mat-checkbox-indeterminate.mat-primary .mat-checkbox-background{background-color:#3f51b5}.mat-checkbox-checked.mat-accent .mat-checkbox-background,.mat-checkbox-indeterminate.mat-accent .mat-checkbox-background{background-color:#ff4081}.mat-checkbox-checked.mat-warn .mat-checkbox-background,.mat-checkbox-indeterminate.mat-warn .mat-checkbox-background{background-color:#f44336}.mat-checkbox-disabled.mat-checkbox-checked .mat-checkbox-background,.mat-checkbox-disabled.mat-checkbox-indeterminate .mat-checkbox-background{background-color:#b0b0b0}.mat-checkbox-disabled:not(.mat-checkbox-checked) .mat-checkbox-frame{border-color:#b0b0b0}.mat-checkbox-disabled .mat-checkbox-label{color:rgba(0,0,0,.54)}.mat-checkbox .mat-ripple-element{background-color:#000}.mat-checkbox-checked:not(.mat-checkbox-disabled).mat-primary .mat-ripple-element,.mat-checkbox:active:not(.mat-checkbox-disabled).mat-primary .mat-ripple-element{background:#3f51b5}.mat-checkbox-checked:not(.mat-checkbox-disabled).mat-accent .mat-ripple-element,.mat-checkbox:active:not(.mat-checkbox-disabled).mat-accent .mat-ripple-element{background:#ff4081}.mat-checkbox-checked:not(.mat-checkbox-disabled).mat-warn .mat-ripple-element,.mat-checkbox:active:not(.mat-checkbox-disabled).mat-warn .mat-ripple-element{background:#f44336}.mat-chip.mat-standard-chip{background-color:#e0e0e0;color:rgba(0,0,0,.87)}.mat-chip.mat-standard-chip .mat-chip-remove{color:rgba(0,0,0,.87);opacity:.4}.mat-chip.mat-standard-chip:not(.mat-chip-disabled):active{box-shadow:0 3px 3px -2px rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 1px 8px 0 rgba(0,0,0,.12)}.mat-chip.mat-standard-chip:not(.mat-chip-disabled) .mat-chip-remove:hover{opacity:.54}.mat-chip.mat-standard-chip.mat-chip-disabled{opacity:.4}.mat-chip.mat-standard-chip:after{background:#000}.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary{background-color:#3f51b5;color:#fff}.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary .mat-chip-remove{color:#fff;opacity:.4}.mat-chip.mat-standard-chip.mat-chip-selected.mat-primary .mat-ripple-element{background-color:hsla(0,0%,100%,.1)}.mat-chip.mat-standard-chip.mat-chip-selected.mat-warn{background-color:#f44336;color:#fff}.mat-chip.mat-standard-chip.mat-chip-selected.mat-warn .mat-chip-remove{color:#fff;opacity:.4}.mat-chip.mat-standard-chip.mat-chip-selected.mat-warn .mat-ripple-element{background-color:hsla(0,0%,100%,.1)}.mat-chip.mat-standard-chip.mat-chip-selected.mat-accent{background-color:#ff4081;color:#fff}.mat-chip.mat-standard-chip.mat-chip-selected.mat-accent .mat-chip-remove{color:#fff;opacity:.4}.mat-chip.mat-standard-chip.mat-chip-selected.mat-accent .mat-ripple-element{background-color:hsla(0,0%,100%,.1)}.mat-table{background:#fff}.mat-table-sticky,.mat-table tbody,.mat-table tfoot,.mat-table thead,[mat-footer-row],[mat-header-row],[mat-row],mat-footer-row,mat-header-row,mat-row{background:inherit}mat-footer-row,mat-header-row,mat-row,td.mat-cell,td.mat-footer-cell,th.mat-header-cell{border-bottom-color:rgba(0,0,0,.12)}.mat-header-cell{color:rgba(0,0,0,.54)}.mat-cell,.mat-footer-cell{color:rgba(0,0,0,.87)}.mat-calendar-arrow{border-top-color:rgba(0,0,0,.54)}.mat-datepicker-content .mat-calendar-next-button,.mat-datepicker-content .mat-calendar-previous-button,.mat-datepicker-toggle{color:rgba(0,0,0,.54)}.mat-calendar-table-header{color:rgba(0,0,0,.38)}.mat-calendar-table-header-divider:after{background:rgba(0,0,0,.12)}.mat-calendar-body-label{color:rgba(0,0,0,.54)}.mat-calendar-body-cell-content,.mat-date-range-input-separator{color:rgba(0,0,0,.87);border-color:transparent}.mat-calendar-body-disabled>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.mat-form-field-disabled .mat-date-range-input-separator{color:rgba(0,0,0,.38)}.mat-calendar-body-in-preview{color:rgba(0,0,0,.24)}.mat-calendar-body-today:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){border-color:rgba(0,0,0,.38)}.mat-calendar-body-disabled>.mat-calendar-body-today:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){border-color:rgba(0,0,0,.18)}.mat-calendar-body-in-range:before{background:rgba(63,81,181,.2)}.mat-calendar-body-comparison-identical,.mat-calendar-body-in-comparison-range:before{background:rgba(249,171,0,.2)}.mat-calendar-body-comparison-bridge-start:before,[dir=rtl] .mat-calendar-body-comparison-bridge-end:before{background:linear-gradient(90deg,rgba(63,81,181,.2) 50%,rgba(249,171,0,.2) 0)}.mat-calendar-body-comparison-bridge-end:before,[dir=rtl] .mat-calendar-body-comparison-bridge-start:before{background:linear-gradient(270deg,rgba(63,81,181,.2) 50%,rgba(249,171,0,.2) 0)}.mat-calendar-body-in-comparison-range.mat-calendar-body-in-range:after,.mat-calendar-body-in-range>.mat-calendar-body-comparison-identical{background:#a8dab5}.mat-calendar-body-comparison-identical.mat-calendar-body-selected,.mat-calendar-body-in-comparison-range>.mat-calendar-body-selected{background:#46a35e}.mat-calendar-body-selected{background-color:#3f51b5;color:#fff}.mat-calendar-body-disabled>.mat-calendar-body-selected{background-color:rgba(63,81,181,.4)}.mat-calendar-body-today.mat-calendar-body-selected{box-shadow:inset 0 0 0 1px #fff}.cdk-keyboard-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.cdk-program-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){background-color:rgba(63,81,181,.3)}.mat-datepicker-content{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12);background-color:#fff;color:rgba(0,0,0,.87)}.mat-datepicker-content.mat-accent .mat-calendar-body-in-range:before{background:rgba(255,64,129,.2)}.mat-datepicker-content.mat-accent .mat-calendar-body-comparison-identical,.mat-datepicker-content.mat-accent .mat-calendar-body-in-comparison-range:before{background:rgba(249,171,0,.2)}.mat-datepicker-content.mat-accent .mat-calendar-body-comparison-bridge-start:before,.mat-datepicker-content.mat-accent [dir=rtl] .mat-calendar-body-comparison-bridge-end:before{background:linear-gradient(90deg,rgba(255,64,129,.2) 50%,rgba(249,171,0,.2) 0)}.mat-datepicker-content.mat-accent .mat-calendar-body-comparison-bridge-end:before,.mat-datepicker-content.mat-accent [dir=rtl] .mat-calendar-body-comparison-bridge-start:before{background:linear-gradient(270deg,rgba(255,64,129,.2) 50%,rgba(249,171,0,.2) 0)}.mat-datepicker-content.mat-accent .mat-calendar-body-in-comparison-range.mat-calendar-body-in-range:after,.mat-datepicker-content.mat-accent .mat-calendar-body-in-range>.mat-calendar-body-comparison-identical{background:#a8dab5}.mat-datepicker-content.mat-accent .mat-calendar-body-comparison-identical.mat-calendar-body-selected,.mat-datepicker-content.mat-accent .mat-calendar-body-in-comparison-range>.mat-calendar-body-selected{background:#46a35e}.mat-datepicker-content.mat-accent .mat-calendar-body-selected{background-color:#ff4081;color:#fff}.mat-datepicker-content.mat-accent .mat-calendar-body-disabled>.mat-calendar-body-selected{background-color:rgba(255,64,129,.4)}.mat-datepicker-content.mat-accent .mat-calendar-body-today.mat-calendar-body-selected{box-shadow:inset 0 0 0 1px #fff}.mat-datepicker-content.mat-accent .cdk-keyboard-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.mat-datepicker-content.mat-accent .cdk-program-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.mat-datepicker-content.mat-accent .mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){background-color:rgba(255,64,129,.3)}.mat-datepicker-content.mat-warn .mat-calendar-body-in-range:before{background:rgba(244,67,54,.2)}.mat-datepicker-content.mat-warn .mat-calendar-body-comparison-identical,.mat-datepicker-content.mat-warn .mat-calendar-body-in-comparison-range:before{background:rgba(249,171,0,.2)}.mat-datepicker-content.mat-warn .mat-calendar-body-comparison-bridge-start:before,.mat-datepicker-content.mat-warn [dir=rtl] .mat-calendar-body-comparison-bridge-end:before{background:linear-gradient(90deg,rgba(244,67,54,.2) 50%,rgba(249,171,0,.2) 0)}.mat-datepicker-content.mat-warn .mat-calendar-body-comparison-bridge-end:before,.mat-datepicker-content.mat-warn [dir=rtl] .mat-calendar-body-comparison-bridge-start:before{background:linear-gradient(270deg,rgba(244,67,54,.2) 50%,rgba(249,171,0,.2) 0)}.mat-datepicker-content.mat-warn .mat-calendar-body-in-comparison-range.mat-calendar-body-in-range:after,.mat-datepicker-content.mat-warn .mat-calendar-body-in-range>.mat-calendar-body-comparison-identical{background:#a8dab5}.mat-datepicker-content.mat-warn .mat-calendar-body-comparison-identical.mat-calendar-body-selected,.mat-datepicker-content.mat-warn .mat-calendar-body-in-comparison-range>.mat-calendar-body-selected{background:#46a35e}.mat-datepicker-content.mat-warn .mat-calendar-body-selected{background-color:#f44336;color:#fff}.mat-datepicker-content.mat-warn .mat-calendar-body-disabled>.mat-calendar-body-selected{background-color:rgba(244,67,54,.4)}.mat-datepicker-content.mat-warn .mat-calendar-body-today.mat-calendar-body-selected{box-shadow:inset 0 0 0 1px #fff}.mat-datepicker-content.mat-warn .cdk-keyboard-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.mat-datepicker-content.mat-warn .cdk-program-focused .mat-calendar-body-active>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical),.mat-datepicker-content.mat-warn .mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover>.mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical){background-color:rgba(244,67,54,.3)}.mat-datepicker-content-touch{box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12)}.mat-datepicker-toggle-active{color:#3f51b5}.mat-datepicker-toggle-active.mat-accent{color:#ff4081}.mat-datepicker-toggle-active.mat-warn{color:#f44336}.mat-date-range-input-inner[disabled]{color:rgba(0,0,0,.38)}.mat-dialog-container{box-shadow:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12);background:#fff;color:rgba(0,0,0,.87)}.mat-divider{border-top-color:rgba(0,0,0,.12)}.mat-divider-vertical{border-right-color:rgba(0,0,0,.12)}.mat-expansion-panel{background:#fff;color:rgba(0,0,0,.87)}.mat-expansion-panel:not([class*=mat-elevation-z]){box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}.mat-action-row{border-top-color:rgba(0,0,0,.12)}.mat-expansion-panel .mat-expansion-panel-header.cdk-keyboard-focused:not([aria-disabled=true]),.mat-expansion-panel .mat-expansion-panel-header.cdk-program-focused:not([aria-disabled=true]),.mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:hover:not([aria-disabled=true]){background:rgba(0,0,0,.04)}@media (hover:none){.mat-expansion-panel:not(.mat-expanded):not([aria-disabled=true]) .mat-expansion-panel-header:hover{background:#fff}}.mat-expansion-panel-header-title{color:rgba(0,0,0,.87)}.mat-expansion-indicator:after,.mat-expansion-panel-header-description{color:rgba(0,0,0,.54)}.mat-expansion-panel-header[aria-disabled=true]{color:rgba(0,0,0,.26)}.mat-expansion-panel-header[aria-disabled=true] .mat-expansion-panel-header-description,.mat-expansion-panel-header[aria-disabled=true] .mat-expansion-panel-header-title{color:inherit}.mat-expansion-panel-header{height:48px}.mat-expansion-panel-header.mat-expanded{height:64px}.mat-form-field-label,.mat-hint{color:rgba(0,0,0,.6)}.mat-form-field.mat-focused .mat-form-field-label{color:#3f51b5}.mat-form-field.mat-focused .mat-form-field-label.mat-accent{color:#ff4081}.mat-form-field.mat-focused .mat-form-field-label.mat-warn{color:#f44336}.mat-focused .mat-form-field-required-marker{color:#ff4081}.mat-form-field-ripple{background-color:rgba(0,0,0,.87)}.mat-form-field.mat-focused .mat-form-field-ripple{background-color:#3f51b5}.mat-form-field.mat-focused .mat-form-field-ripple.mat-accent{background-color:#ff4081}.mat-form-field.mat-focused .mat-form-field-ripple.mat-warn{background-color:#f44336}.mat-form-field-type-mat-native-select.mat-focused:not(.mat-form-field-invalid) .mat-form-field-infix:after{color:#3f51b5}.mat-form-field-type-mat-native-select.mat-focused:not(.mat-form-field-invalid).mat-accent .mat-form-field-infix:after{color:#ff4081}.mat-form-field-type-mat-native-select.mat-focused:not(.mat-form-field-invalid).mat-warn .mat-form-field-infix:after,.mat-form-field.mat-form-field-invalid .mat-form-field-label,.mat-form-field.mat-form-field-invalid .mat-form-field-label.mat-accent,.mat-form-field.mat-form-field-invalid .mat-form-field-label .mat-form-field-required-marker{color:#f44336}.mat-form-field.mat-form-field-invalid .mat-form-field-ripple,.mat-form-field.mat-form-field-invalid .mat-form-field-ripple.mat-accent{background-color:#f44336}.mat-error{color:#f44336}.mat-form-field-appearance-legacy .mat-form-field-label,.mat-form-field-appearance-legacy .mat-hint{color:rgba(0,0,0,.54)}.mat-form-field-appearance-legacy .mat-form-field-underline{background-color:rgba(0,0,0,.42)}.mat-form-field-appearance-legacy.mat-form-field-disabled .mat-form-field-underline{background-image:linear-gradient(90deg,rgba(0,0,0,.42) 0,rgba(0,0,0,.42) 33%,transparent 0);background-size:4px 100%;background-repeat:repeat-x}.mat-form-field-appearance-standard .mat-form-field-underline{background-color:rgba(0,0,0,.42)}.mat-form-field-appearance-standard.mat-form-field-disabled .mat-form-field-underline{background-image:linear-gradient(90deg,rgba(0,0,0,.42) 0,rgba(0,0,0,.42) 33%,transparent 0);background-size:4px 100%;background-repeat:repeat-x}.mat-form-field-appearance-fill .mat-form-field-flex{background-color:rgba(0,0,0,.04)}.mat-form-field-appearance-fill.mat-form-field-disabled .mat-form-field-flex{background-color:rgba(0,0,0,.02)}.mat-form-field-appearance-fill .mat-form-field-underline:before{background-color:rgba(0,0,0,.42)}.mat-form-field-appearance-fill.mat-form-field-disabled .mat-form-field-label{color:rgba(0,0,0,.38)}.mat-form-field-appearance-fill.mat-form-field-disabled .mat-form-field-underline:before{background-color:initial}.mat-form-field-appearance-outline .mat-form-field-outline{color:rgba(0,0,0,.12)}.mat-form-field-appearance-outline .mat-form-field-outline-thick{color:rgba(0,0,0,.87)}.mat-form-field-appearance-outline.mat-focused .mat-form-field-outline-thick{color:#3f51b5}.mat-form-field-appearance-outline.mat-focused.mat-accent .mat-form-field-outline-thick{color:#ff4081}.mat-form-field-appearance-outline.mat-focused.mat-warn .mat-form-field-outline-thick,.mat-form-field-appearance-outline.mat-form-field-invalid.mat-form-field-invalid .mat-form-field-outline-thick{color:#f44336}.mat-form-field-appearance-outline.mat-form-field-disabled .mat-form-field-label{color:rgba(0,0,0,.38)}.mat-form-field-appearance-outline.mat-form-field-disabled .mat-form-field-outline{color:rgba(0,0,0,.06)}.mat-icon.mat-primary{color:#3f51b5}.mat-icon.mat-accent{color:#ff4081}.mat-icon.mat-warn{color:#f44336}.mat-form-field-type-mat-native-select .mat-form-field-infix:after{color:rgba(0,0,0,.54)}.mat-form-field-type-mat-native-select.mat-form-field-disabled .mat-form-field-infix:after,.mat-input-element:disabled{color:rgba(0,0,0,.38)}.mat-input-element{caret-color:#3f51b5}.mat-input-element::placeholder{color:rgba(0,0,0,.42)}.mat-input-element::-moz-placeholder{color:rgba(0,0,0,.42)}.mat-input-element::-webkit-input-placeholder{color:rgba(0,0,0,.42)}.mat-input-element:-ms-input-placeholder{color:rgba(0,0,0,.42)}.mat-form-field.mat-accent .mat-input-element{caret-color:#ff4081}.mat-form-field-invalid .mat-input-element,.mat-form-field.mat-warn .mat-input-element{caret-color:#f44336}.mat-form-field-type-mat-native-select.mat-form-field-invalid .mat-form-field-infix:after{color:#f44336}.mat-list-base .mat-list-item,.mat-list-base .mat-list-option{color:rgba(0,0,0,.87)}.mat-list-base .mat-subheader{color:rgba(0,0,0,.54)}.mat-list-item-disabled{background-color:#eee}.mat-action-list .mat-list-item:focus,.mat-action-list .mat-list-item:hover,.mat-list-option:focus,.mat-list-option:hover,.mat-nav-list .mat-list-item:focus,.mat-nav-list .mat-list-item:hover{background:rgba(0,0,0,.04)}.mat-list-single-selected-option,.mat-list-single-selected-option:focus,.mat-list-single-selected-option:hover{background:rgba(0,0,0,.12)}.mat-menu-panel{background:#fff}.mat-menu-panel:not([class*=mat-elevation-z]){box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.mat-menu-item{background:transparent;color:rgba(0,0,0,.87)}.mat-menu-item[disabled],.mat-menu-item[disabled] .mat-icon-no-color,.mat-menu-item[disabled]:after{color:rgba(0,0,0,.38)}.mat-menu-item-submenu-trigger:after,.mat-menu-item .mat-icon-no-color{color:rgba(0,0,0,.54)}.mat-menu-item-highlighted:not([disabled]),.mat-menu-item.cdk-keyboard-focused:not([disabled]),.mat-menu-item.cdk-program-focused:not([disabled]),.mat-menu-item:hover:not([disabled]){background:rgba(0,0,0,.04)}.mat-paginator{background:#fff}.mat-paginator,.mat-paginator-page-size .mat-select-trigger{color:rgba(0,0,0,.54)}.mat-paginator-decrement,.mat-paginator-increment{border-top:2px solid rgba(0,0,0,.54);border-right:2px solid rgba(0,0,0,.54)}.mat-paginator-first,.mat-paginator-last{border-top:2px solid rgba(0,0,0,.54)}.mat-icon-button[disabled] .mat-paginator-decrement,.mat-icon-button[disabled] .mat-paginator-first,.mat-icon-button[disabled] .mat-paginator-increment,.mat-icon-button[disabled] .mat-paginator-last{border-color:rgba(0,0,0,.38)}.mat-paginator-container{min-height:56px}.mat-progress-bar-background{fill:#c5cae9}.mat-progress-bar-buffer{background-color:#c5cae9}.mat-progress-bar-fill:after{background-color:#3f51b5}.mat-progress-bar.mat-accent .mat-progress-bar-background{fill:#ff80ab}.mat-progress-bar.mat-accent .mat-progress-bar-buffer{background-color:#ff80ab}.mat-progress-bar.mat-accent .mat-progress-bar-fill:after{background-color:#ff4081}.mat-progress-bar.mat-warn .mat-progress-bar-background{fill:#ffcdd2}.mat-progress-bar.mat-warn .mat-progress-bar-buffer{background-color:#ffcdd2}.mat-progress-bar.mat-warn .mat-progress-bar-fill:after{background-color:#f44336}.mat-progress-spinner circle,.mat-spinner circle{stroke:#3f51b5}.mat-progress-spinner.mat-accent circle,.mat-spinner.mat-accent circle{stroke:#ff4081}.mat-progress-spinner.mat-warn circle,.mat-spinner.mat-warn circle{stroke:#f44336}.mat-radio-outer-circle{border-color:rgba(0,0,0,.54)}.mat-radio-button.mat-primary.mat-radio-checked .mat-radio-outer-circle{border-color:#3f51b5}.mat-radio-button.mat-primary.mat-radio-checked .mat-radio-persistent-ripple,.mat-radio-button.mat-primary .mat-radio-inner-circle,.mat-radio-button.mat-primary .mat-radio-ripple .mat-ripple-element:not(.mat-radio-persistent-ripple),.mat-radio-button.mat-primary:active .mat-radio-persistent-ripple{background-color:#3f51b5}.mat-radio-button.mat-accent.mat-radio-checked .mat-radio-outer-circle{border-color:#ff4081}.mat-radio-button.mat-accent.mat-radio-checked .mat-radio-persistent-ripple,.mat-radio-button.mat-accent .mat-radio-inner-circle,.mat-radio-button.mat-accent .mat-radio-ripple .mat-ripple-element:not(.mat-radio-persistent-ripple),.mat-radio-button.mat-accent:active .mat-radio-persistent-ripple{background-color:#ff4081}.mat-radio-button.mat-warn.mat-radio-checked .mat-radio-outer-circle{border-color:#f44336}.mat-radio-button.mat-warn.mat-radio-checked .mat-radio-persistent-ripple,.mat-radio-button.mat-warn .mat-radio-inner-circle,.mat-radio-button.mat-warn .mat-radio-ripple .mat-ripple-element:not(.mat-radio-persistent-ripple),.mat-radio-button.mat-warn:active .mat-radio-persistent-ripple{background-color:#f44336}.mat-radio-button.mat-radio-disabled.mat-radio-checked .mat-radio-outer-circle,.mat-radio-button.mat-radio-disabled .mat-radio-outer-circle{border-color:rgba(0,0,0,.38)}.mat-radio-button.mat-radio-disabled .mat-radio-inner-circle,.mat-radio-button.mat-radio-disabled .mat-radio-ripple .mat-ripple-element{background-color:rgba(0,0,0,.38)}.mat-radio-button.mat-radio-disabled .mat-radio-label-content{color:rgba(0,0,0,.38)}.mat-radio-button .mat-ripple-element{background-color:#000}.mat-select-value{color:rgba(0,0,0,.87)}.mat-select-placeholder{color:rgba(0,0,0,.42)}.mat-select-disabled .mat-select-value{color:rgba(0,0,0,.38)}.mat-select-arrow{color:rgba(0,0,0,.54)}.mat-select-panel{background:#fff}.mat-select-panel:not([class*=mat-elevation-z]){box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.mat-select-panel .mat-option.mat-selected:not(.mat-option-multiple){background:rgba(0,0,0,.12)}.mat-form-field.mat-focused.mat-primary .mat-select-arrow{color:#3f51b5}.mat-form-field.mat-focused.mat-accent .mat-select-arrow{color:#ff4081}.mat-form-field.mat-focused.mat-warn .mat-select-arrow,.mat-form-field .mat-select.mat-select-invalid .mat-select-arrow{color:#f44336}.mat-form-field .mat-select.mat-select-disabled .mat-select-arrow{color:rgba(0,0,0,.38)}.mat-drawer-container{background-color:#fafafa;color:rgba(0,0,0,.87)}.mat-drawer{color:rgba(0,0,0,.87)}.mat-drawer,.mat-drawer.mat-drawer-push{background-color:#fff}.mat-drawer:not(.mat-drawer-side){box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12)}.mat-drawer-side{border-right:1px solid rgba(0,0,0,.12)}.mat-drawer-side.mat-drawer-end,[dir=rtl] .mat-drawer-side{border-left:1px solid rgba(0,0,0,.12);border-right:none}[dir=rtl] .mat-drawer-side.mat-drawer-end{border-left:none;border-right:1px solid rgba(0,0,0,.12)}.mat-drawer-backdrop.mat-drawer-shown{background-color:rgba(0,0,0,.6)}.mat-slide-toggle.mat-checked .mat-slide-toggle-thumb{background-color:#ff4081}.mat-slide-toggle.mat-checked .mat-slide-toggle-bar{background-color:rgba(255,64,129,.54)}.mat-slide-toggle.mat-checked .mat-ripple-element{background-color:#ff4081}.mat-slide-toggle.mat-primary.mat-checked .mat-slide-toggle-thumb{background-color:#3f51b5}.mat-slide-toggle.mat-primary.mat-checked .mat-slide-toggle-bar{background-color:rgba(63,81,181,.54)}.mat-slide-toggle.mat-primary.mat-checked .mat-ripple-element{background-color:#3f51b5}.mat-slide-toggle.mat-warn.mat-checked .mat-slide-toggle-thumb{background-color:#f44336}.mat-slide-toggle.mat-warn.mat-checked .mat-slide-toggle-bar{background-color:rgba(244,67,54,.54)}.mat-slide-toggle.mat-warn.mat-checked .mat-ripple-element{background-color:#f44336}.mat-slide-toggle:not(.mat-checked) .mat-ripple-element{background-color:#000}.mat-slide-toggle-thumb{box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);background-color:#fafafa}.mat-slide-toggle-bar{background-color:rgba(0,0,0,.38)}.mat-slider-track-background{background-color:rgba(0,0,0,.26)}.mat-primary .mat-slider-thumb,.mat-primary .mat-slider-thumb-label,.mat-primary .mat-slider-track-fill{background-color:#3f51b5}.mat-primary .mat-slider-thumb-label-text{color:#fff}.mat-primary .mat-slider-focus-ring{background-color:rgba(63,81,181,.2)}.mat-accent .mat-slider-thumb,.mat-accent .mat-slider-thumb-label,.mat-accent .mat-slider-track-fill{background-color:#ff4081}.mat-accent .mat-slider-thumb-label-text{color:#fff}.mat-accent .mat-slider-focus-ring{background-color:rgba(255,64,129,.2)}.mat-warn .mat-slider-thumb,.mat-warn .mat-slider-thumb-label,.mat-warn .mat-slider-track-fill{background-color:#f44336}.mat-warn .mat-slider-thumb-label-text{color:#fff}.mat-warn .mat-slider-focus-ring{background-color:rgba(244,67,54,.2)}.cdk-focused .mat-slider-track-background,.mat-slider:hover .mat-slider-track-background{background-color:rgba(0,0,0,.38)}.mat-slider-disabled .mat-slider-thumb,.mat-slider-disabled .mat-slider-track-background,.mat-slider-disabled .mat-slider-track-fill,.mat-slider-disabled:hover .mat-slider-track-background{background-color:rgba(0,0,0,.26)}.mat-slider-min-value .mat-slider-focus-ring{background-color:rgba(0,0,0,.12)}.mat-slider-min-value.mat-slider-thumb-label-showing .mat-slider-thumb,.mat-slider-min-value.mat-slider-thumb-label-showing .mat-slider-thumb-label{background-color:rgba(0,0,0,.87)}.mat-slider-min-value.mat-slider-thumb-label-showing.cdk-focused .mat-slider-thumb,.mat-slider-min-value.mat-slider-thumb-label-showing.cdk-focused .mat-slider-thumb-label{background-color:rgba(0,0,0,.26)}.mat-slider-min-value:not(.mat-slider-thumb-label-showing) .mat-slider-thumb{border-color:rgba(0,0,0,.26);background-color:initial}.mat-slider-min-value:not(.mat-slider-thumb-label-showing).cdk-focused .mat-slider-thumb,.mat-slider-min-value:not(.mat-slider-thumb-label-showing):hover .mat-slider-thumb{border-color:rgba(0,0,0,.38)}.mat-slider-min-value:not(.mat-slider-thumb-label-showing).cdk-focused.mat-slider-disabled .mat-slider-thumb,.mat-slider-min-value:not(.mat-slider-thumb-label-showing):hover.mat-slider-disabled .mat-slider-thumb{border-color:rgba(0,0,0,.26)}.mat-slider-has-ticks .mat-slider-wrapper:after{border-color:rgba(0,0,0,.7)}.mat-slider-horizontal .mat-slider-ticks{background-image:repeating-linear-gradient(90deg,rgba(0,0,0,.7),rgba(0,0,0,.7) 2px,transparent 0,transparent);background-image:-moz-repeating-linear-gradient(.0001deg,rgba(0,0,0,.7),rgba(0,0,0,.7) 2px,transparent 0,transparent)}.mat-slider-vertical .mat-slider-ticks{background-image:repeating-linear-gradient(180deg,rgba(0,0,0,.7),rgba(0,0,0,.7) 2px,transparent 0,transparent)}.mat-step-header.cdk-keyboard-focused,.mat-step-header.cdk-program-focused,.mat-step-header:hover{background-color:rgba(0,0,0,.04)}@media (hover:none){.mat-step-header:hover{background:none}}.mat-step-header .mat-step-label,.mat-step-header .mat-step-optional{color:rgba(0,0,0,.54)}.mat-step-header .mat-step-icon{background-color:rgba(0,0,0,.54);color:#fff}.mat-step-header .mat-step-icon-selected,.mat-step-header .mat-step-icon-state-done,.mat-step-header .mat-step-icon-state-edit{background-color:#3f51b5;color:#fff}.mat-step-header.mat-accent .mat-step-icon{color:#fff}.mat-step-header.mat-accent .mat-step-icon-selected,.mat-step-header.mat-accent .mat-step-icon-state-done,.mat-step-header.mat-accent .mat-step-icon-state-edit{background-color:#ff4081;color:#fff}.mat-step-header.mat-warn .mat-step-icon{color:#fff}.mat-step-header.mat-warn .mat-step-icon-selected,.mat-step-header.mat-warn .mat-step-icon-state-done,.mat-step-header.mat-warn .mat-step-icon-state-edit{background-color:#f44336;color:#fff}.mat-step-header .mat-step-icon-state-error{background-color:initial;color:#f44336}.mat-step-header .mat-step-label.mat-step-label-active{color:rgba(0,0,0,.87)}.mat-step-header .mat-step-label.mat-step-label-error{color:#f44336}.mat-stepper-horizontal,.mat-stepper-vertical{background-color:#fff}.mat-stepper-vertical-line:before{border-left-color:rgba(0,0,0,.12)}.mat-horizontal-stepper-header:after,.mat-horizontal-stepper-header:before,.mat-stepper-horizontal-line{border-top-color:rgba(0,0,0,.12)}.mat-horizontal-stepper-header{height:72px}.mat-stepper-label-position-bottom .mat-horizontal-stepper-header,.mat-vertical-stepper-header{padding:24px}.mat-stepper-vertical-line:before{top:-16px;bottom:-16px}.mat-stepper-label-position-bottom .mat-horizontal-stepper-header:after,.mat-stepper-label-position-bottom .mat-horizontal-stepper-header:before,.mat-stepper-label-position-bottom .mat-stepper-horizontal-line{top:36px}.mat-sort-header-arrow{color:#757575}.mat-tab-header,.mat-tab-nav-bar{border-bottom:1px solid rgba(0,0,0,.12)}.mat-tab-group-inverted-header .mat-tab-header,.mat-tab-group-inverted-header .mat-tab-nav-bar{border-top:1px solid rgba(0,0,0,.12);border-bottom:none}.mat-tab-label,.mat-tab-link{color:rgba(0,0,0,.87)}.mat-tab-label.mat-tab-disabled,.mat-tab-link.mat-tab-disabled{color:rgba(0,0,0,.38)}.mat-tab-header-pagination-chevron{border-color:rgba(0,0,0,.87)}.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron{border-color:rgba(0,0,0,.38)}.mat-tab-group[class*=mat-background-] .mat-tab-header,.mat-tab-nav-bar[class*=mat-background-]{border-bottom:none;border-top:none}.mat-tab-group.mat-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-group.mat-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled){background-color:rgba(197,202,233,.3)}.mat-tab-group.mat-primary .mat-ink-bar,.mat-tab-nav-bar.mat-primary .mat-ink-bar{background-color:#3f51b5}.mat-tab-group.mat-primary.mat-background-primary>.mat-tab-header .mat-ink-bar,.mat-tab-group.mat-primary.mat-background-primary>.mat-tab-link-container .mat-ink-bar,.mat-tab-nav-bar.mat-primary.mat-background-primary>.mat-tab-header .mat-ink-bar,.mat-tab-nav-bar.mat-primary.mat-background-primary>.mat-tab-link-container .mat-ink-bar{background-color:#fff}.mat-tab-group.mat-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-group.mat-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled){background-color:rgba(255,128,171,.3)}.mat-tab-group.mat-accent .mat-ink-bar,.mat-tab-nav-bar.mat-accent .mat-ink-bar{background-color:#ff4081}.mat-tab-group.mat-accent.mat-background-accent>.mat-tab-header .mat-ink-bar,.mat-tab-group.mat-accent.mat-background-accent>.mat-tab-link-container .mat-ink-bar,.mat-tab-nav-bar.mat-accent.mat-background-accent>.mat-tab-header .mat-ink-bar,.mat-tab-nav-bar.mat-accent.mat-background-accent>.mat-tab-link-container .mat-ink-bar{background-color:#fff}.mat-tab-group.mat-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-group.mat-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled){background-color:rgba(255,205,210,.3)}.mat-tab-group.mat-warn .mat-ink-bar,.mat-tab-nav-bar.mat-warn .mat-ink-bar{background-color:#f44336}.mat-tab-group.mat-warn.mat-background-warn>.mat-tab-header .mat-ink-bar,.mat-tab-group.mat-warn.mat-background-warn>.mat-tab-link-container .mat-ink-bar,.mat-tab-nav-bar.mat-warn.mat-background-warn>.mat-tab-header .mat-ink-bar,.mat-tab-nav-bar.mat-warn.mat-background-warn>.mat-tab-link-container .mat-ink-bar{background-color:#fff}.mat-tab-group.mat-background-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled){background-color:rgba(197,202,233,.3)}.mat-tab-group.mat-background-primary>.mat-tab-header,.mat-tab-group.mat-background-primary>.mat-tab-header-pagination,.mat-tab-group.mat-background-primary>.mat-tab-link-container,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header-pagination,.mat-tab-nav-bar.mat-background-primary>.mat-tab-link-container{background-color:#3f51b5}.mat-tab-group.mat-background-primary>.mat-tab-header .mat-tab-label,.mat-tab-group.mat-background-primary>.mat-tab-link-container .mat-tab-link,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header .mat-tab-label,.mat-tab-nav-bar.mat-background-primary>.mat-tab-link-container .mat-tab-link{color:#fff}.mat-tab-group.mat-background-primary>.mat-tab-header .mat-tab-label.mat-tab-disabled,.mat-tab-group.mat-background-primary>.mat-tab-link-container .mat-tab-link.mat-tab-disabled,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header .mat-tab-label.mat-tab-disabled,.mat-tab-nav-bar.mat-background-primary>.mat-tab-link-container .mat-tab-link.mat-tab-disabled{color:hsla(0,0%,100%,.4)}.mat-tab-group.mat-background-primary>.mat-tab-header-pagination .mat-tab-header-pagination-chevron,.mat-tab-group.mat-background-primary>.mat-tab-header .mat-focus-indicator:before,.mat-tab-group.mat-background-primary>.mat-tab-links .mat-focus-indicator:before,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header-pagination .mat-tab-header-pagination-chevron,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header .mat-focus-indicator:before,.mat-tab-nav-bar.mat-background-primary>.mat-tab-links .mat-focus-indicator:before{border-color:#fff}.mat-tab-group.mat-background-primary>.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron{border-color:hsla(0,0%,100%,.4)}.mat-tab-group.mat-background-primary>.mat-tab-header .mat-ripple-element,.mat-tab-group.mat-background-primary>.mat-tab-link-container .mat-ripple-element,.mat-tab-nav-bar.mat-background-primary>.mat-tab-header .mat-ripple-element,.mat-tab-nav-bar.mat-background-primary>.mat-tab-link-container .mat-ripple-element{background-color:hsla(0,0%,100%,.12)}.mat-tab-group.mat-background-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled){background-color:rgba(255,128,171,.3)}.mat-tab-group.mat-background-accent>.mat-tab-header,.mat-tab-group.mat-background-accent>.mat-tab-header-pagination,.mat-tab-group.mat-background-accent>.mat-tab-link-container,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header-pagination,.mat-tab-nav-bar.mat-background-accent>.mat-tab-link-container{background-color:#ff4081}.mat-tab-group.mat-background-accent>.mat-tab-header .mat-tab-label,.mat-tab-group.mat-background-accent>.mat-tab-link-container .mat-tab-link,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header .mat-tab-label,.mat-tab-nav-bar.mat-background-accent>.mat-tab-link-container .mat-tab-link{color:#fff}.mat-tab-group.mat-background-accent>.mat-tab-header .mat-tab-label.mat-tab-disabled,.mat-tab-group.mat-background-accent>.mat-tab-link-container .mat-tab-link.mat-tab-disabled,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header .mat-tab-label.mat-tab-disabled,.mat-tab-nav-bar.mat-background-accent>.mat-tab-link-container .mat-tab-link.mat-tab-disabled{color:hsla(0,0%,100%,.4)}.mat-tab-group.mat-background-accent>.mat-tab-header-pagination .mat-tab-header-pagination-chevron,.mat-tab-group.mat-background-accent>.mat-tab-header .mat-focus-indicator:before,.mat-tab-group.mat-background-accent>.mat-tab-links .mat-focus-indicator:before,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header-pagination .mat-tab-header-pagination-chevron,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header .mat-focus-indicator:before,.mat-tab-nav-bar.mat-background-accent>.mat-tab-links .mat-focus-indicator:before{border-color:#fff}.mat-tab-group.mat-background-accent>.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron{border-color:hsla(0,0%,100%,.4)}.mat-tab-group.mat-background-accent>.mat-tab-header .mat-ripple-element,.mat-tab-group.mat-background-accent>.mat-tab-link-container .mat-ripple-element,.mat-tab-nav-bar.mat-background-accent>.mat-tab-header .mat-ripple-element,.mat-tab-nav-bar.mat-background-accent>.mat-tab-link-container .mat-ripple-element{background-color:hsla(0,0%,100%,.12)}.mat-tab-group.mat-background-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-group.mat-background-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled),.mat-tab-nav-bar.mat-background-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled){background-color:rgba(255,205,210,.3)}.mat-tab-group.mat-background-warn>.mat-tab-header,.mat-tab-group.mat-background-warn>.mat-tab-header-pagination,.mat-tab-group.mat-background-warn>.mat-tab-link-container,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header-pagination,.mat-tab-nav-bar.mat-background-warn>.mat-tab-link-container{background-color:#f44336}.mat-tab-group.mat-background-warn>.mat-tab-header .mat-tab-label,.mat-tab-group.mat-background-warn>.mat-tab-link-container .mat-tab-link,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header .mat-tab-label,.mat-tab-nav-bar.mat-background-warn>.mat-tab-link-container .mat-tab-link{color:#fff}.mat-tab-group.mat-background-warn>.mat-tab-header .mat-tab-label.mat-tab-disabled,.mat-tab-group.mat-background-warn>.mat-tab-link-container .mat-tab-link.mat-tab-disabled,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header .mat-tab-label.mat-tab-disabled,.mat-tab-nav-bar.mat-background-warn>.mat-tab-link-container .mat-tab-link.mat-tab-disabled{color:hsla(0,0%,100%,.4)}.mat-tab-group.mat-background-warn>.mat-tab-header-pagination .mat-tab-header-pagination-chevron,.mat-tab-group.mat-background-warn>.mat-tab-header .mat-focus-indicator:before,.mat-tab-group.mat-background-warn>.mat-tab-links .mat-focus-indicator:before,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header-pagination .mat-tab-header-pagination-chevron,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header .mat-focus-indicator:before,.mat-tab-nav-bar.mat-background-warn>.mat-tab-links .mat-focus-indicator:before{border-color:#fff}.mat-tab-group.mat-background-warn>.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron{border-color:hsla(0,0%,100%,.4)}.mat-tab-group.mat-background-warn>.mat-tab-header .mat-ripple-element,.mat-tab-group.mat-background-warn>.mat-tab-link-container .mat-ripple-element,.mat-tab-nav-bar.mat-background-warn>.mat-tab-header .mat-ripple-element,.mat-tab-nav-bar.mat-background-warn>.mat-tab-link-container .mat-ripple-element{background-color:hsla(0,0%,100%,.12)}.mat-toolbar{background:#f5f5f5;color:rgba(0,0,0,.87)}.mat-toolbar.mat-primary{background:#3f51b5;color:#fff}.mat-toolbar.mat-accent{background:#ff4081;color:#fff}.mat-toolbar.mat-warn{background:#f44336;color:#fff}.mat-toolbar .mat-focused .mat-form-field-ripple,.mat-toolbar .mat-form-field-ripple,.mat-toolbar .mat-form-field-underline{background-color:currentColor}.mat-toolbar .mat-focused .mat-form-field-label,.mat-toolbar .mat-form-field-label,.mat-toolbar .mat-form-field.mat-focused .mat-select-arrow,.mat-toolbar .mat-select-arrow,.mat-toolbar .mat-select-value{color:inherit}.mat-toolbar .mat-input-element{caret-color:currentColor}.mat-toolbar-multiple-rows{min-height:64px}.mat-toolbar-row,.mat-toolbar-single-row{height:64px}@media (max-width:599px){.mat-toolbar-multiple-rows{min-height:56px}.mat-toolbar-row,.mat-toolbar-single-row{height:56px}}.mat-tooltip{background:rgba(97,97,97,.9)}.mat-tree{background:#fff}.mat-nested-tree-node,.mat-tree-node{color:rgba(0,0,0,.87)}.mat-tree-node{min-height:48px}.mat-snack-bar-container{color:hsla(0,0%,100%,.7);background:#323232;box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12)}.mat-simple-snackbar-action{color:#ff4081}
+/*! tailwindcss v2.0.4 | MIT License | https://tailwindcss.com */
+
+/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:initial}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:initial;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.bg-opacity-25{--tw-bg-opacity:0.25}.border-collapse{border-collapse:collapse}.border-white{--tw-border-opacity:1;border-color:rgba(255,255,255,var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-t{border-top-width:1px}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-grow{flex-grow:1}.flex-shrink{flex-shrink:1}.h-0{height:0}.h-2{height:.5rem}.h-10{height:2.5rem}.h-full{height:100%}.m-2{margin:.5rem}.m-4{margin:1rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mb-2{margin-bottom:.5rem}.mr-4{margin-right:1rem}.min-w-0{min-width:0}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.p-2{padding:.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.resize{resize:both}*{--tw-shadow:0 0 transparent}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent)}.text-center{text-align:center}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.italic{font-style:italic}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.underline{text-decoration:underline}.ordinal{--tw-ordinal:var(--tw-empty,/*!*/ /*!*/);--tw-slashed-zero:var(--tw-empty,/*!*/ /*!*/);--tw-numeric-figure:var(--tw-empty,/*!*/ /*!*/);--tw-numeric-spacing:var(--tw-empty,/*!*/ /*!*/);--tw-numeric-fraction:var(--tw-empty,/*!*/ /*!*/);font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);--tw-ordinal:ordinal}.visible{visibility:visible}.invisible{visibility:hidden}.w-0{width:0}.w-2{width:.5rem}.w-1\/2{width:50%}.w-full{width:100%}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@keyframes spin{to{transform:rotate(1turn)}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}body,html{height:100%}body{margin:0;font-family:Roboto,Helvetica Neue,sans-serif}
\ No newline at end of file
diff --git a/www/worker-basic.min.js b/www/worker-basic.min.js
new file mode 100644
index 00000000000..e9606030937
--- /dev/null
+++ b/www/worker-basic.min.js
@@ -0,0 +1,20 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+// tslint:disable:no-console
+
+self.addEventListener('install', event => {
+ self.skipWaiting();
+});
+
+self.addEventListener('activate', event => {
+ event.waitUntil(self.clients.claim());
+ self.registration.unregister().then(() => {
+ console.log('NGSW Safety Worker - unregistered old service worker');
+ });
+});