Full Code of hjdhjd/homebridge-myq for AI

main 9c2d0247af15 cached
37 files
191.8 KB
50.0k tokens
96 symbols
1 requests
Download .txt
Showing preview only (203K chars total). Download the full file or copy to clipboard to get everything.
Repository: hjdhjd/homebridge-myq
Branch: main
Commit: 9c2d0247af15
Files: 37
Total size: 191.8 KB

Directory structure:
gitextract_upv4e1ze/

├── .eslintrc.json
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── auto-merge.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci.yml
│       ├── dependabot-automerge.yml
│       ├── issue-stale.yml
│       ├── issue-validate.yml
│       └── lock-threads.yml
├── .gitignore
├── .npmignore
├── CODE-OF-CONDUCT.md
├── LICENSE.md
├── README.md
├── config.schema.json
├── docs/
│   ├── AdvancedOptions.md
│   ├── Changelog.md
│   ├── FeatureOptions.md
│   └── MQTT.md
├── homebridge-ui/
│   ├── public/
│   │   ├── index.html
│   │   ├── lib/
│   │   │   └── featureoptions.mjs
│   │   ├── myq-featureoptions.mjs
│   │   └── ui.mjs
│   └── server.js
├── nodemon.json
├── package.json
├── src/
│   ├── index.ts
│   ├── myq-device.ts
│   ├── myq-garagedoor.ts
│   ├── myq-lamp.ts
│   ├── myq-mqtt.ts
│   ├── myq-options.ts
│   ├── myq-platform.ts
│   └── settings.ts
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "parserOptions": {
    "ecmaVersion": 2020,
    "project": "tsconfig.json",
    "sourceType": "module"
  },
  "ignorePatterns": [ "dist" ],
  "rules": {
    "brace-style": [ "warn" ],
    "camelcase": [ "warn" ],
    "comma-dangle": [ "error" ],
    "curly": [ "warn", "all" ],
    "dot-notation": "warn",
    "eqeqeq": "warn",
    "indent": [ "warn", 2, { "SwitchCase": 1 } ],
    "linebreak-style": [ "warn", "unix" ],
    "lines-between-class-members": [ "warn", "always", { "exceptAfterSingleLine": true } ],
    "max-len": [ "warn", 170 ],
    "no-await-in-loop": [ "warn" ],
    "no-console": [ "warn" ],
    "prefer-arrow-callback": [ "warn" ],
    "quotes": [ "warn", "double", { "avoidEscape": true } ],
    "semi": [ "warn", "always" ],
    "sort-imports": [ "warn" ],
    "sort-keys": [ "warn" ],
    "sort-vars": [ "warn" ],
    "@typescript-eslint/explicit-function-return-type": [ "warn" ],
    "@typescript-eslint/explicit-module-boundary-types": [ "warn" ],
    "@typescript-eslint/no-explicit-any": [ "warn" ],
    "@typescript-eslint/no-non-null-assertion": [ "warn" ],
    "@typescript-eslint/no-this-alias": [ "warn" ]
  }
}


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Support Request
about: Report a bug or request help. Please read the documentation first before creating a support request.
title: ''
assignees: ''

---

<!-- You must use the issue template below. -->
<!-- Please ensure you read the documentation before creating a support request. -->

**Describe The Problem:**
<!-- A clear and concise description of what the issue is. -->

**To Reproduce:**
<!-- Steps to reproduce the behavior. -->

**Logs:**
<!-- In order to be helpful, include the relevant logs from Homebridge, if applicable. -->

```
Show the Homebridge logs here.
Remove any sensitive information.
```

**Homebridge Configuration:**

```json
Show the relevant portion of your homebridge config.json here, if needed.
Remove any sensitive information.
```

**Screenshots:**
<!-- If applicable, add screenshots to help explain your problem. -->

**Environment:**

* **Homebridge Version:** <!-- homebridge -V -->
* **Node Version:** <!-- node -v -->
* **Homebridge-myQ Plugin Version**:
* **Apple Device and iOS / macOS / iPadOS / tvOS Version:**<!-- Type of Apple device you're using and associated OS version -->
* **Operating System and OS Version:** <!-- Raspbian / Ubuntu / Debian / Windows / macOS / Docker -->

<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->



================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Homebridge Discord Community
    url: https://discord.gg/QXqfHEW
    about: Ask your questions in the myq channel.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request
about: Suggest an idea for an enhancement.
title: ''
labels: enhancement
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. -->

<!-- Click the "Preview" tab before you submit to ensure the formatting is correct. -->


================================================
FILE: .github/auto-merge.yml
================================================
# Merge all dependencies as long within ${TARGET} scope (defined in workflows/dependabot-automerge.yml).
#
- match:
    dependency_type: all
    update_type: semver:minor


================================================
FILE: .github/dependabot.yml
================================================
# Query daily for npm dependency updates.
#
version: 2

updates:

  # Enable version updates for github-actions.
  - package-ecosystem: "github-actions"

    # Look for ".github/workflows" in the "root" directory.
    directory: "/"

    # Check for updated GitHub Actions every weekday.
    schedule:
      interval: "daily"

    # Allow up to ten pull requests to be generated at any one time.
    open-pull-requests-limit: 0

  # Enable version updates for npm.
  - package-ecosystem: "npm"

    # Look for "package.json" and "package-lock.json" files in the "root" directory.
    directory: "/"

    # Check the npm registry for updates every weekday.
    schedule:
      interval: "daily"

    # Allow up to ten pull requests to be generated at any one time.
    open-pull-requests-limit: 0

    # Ignore certain dependency updates.
    # ignore:
      # Ignore mqtt updates for now due to the breaking change in module management.
      # - dependency-name: "mqtt"


================================================
FILE: .github/workflows/ci.yml
================================================
# Continuous integration - validate builds when commits are made, and publish when releases are created.
#
name: "Continuous Integration"

# Run the build on all push, pull request, and release creation events.
on:
  pull_request:
  push:
  release:
    types: [ published ]
  workflow_dispatch:

jobs:

  # Run a validation build on LTS versions of node.
  build:
    name: 'Build package'

    # Build only if we've received a push, manual workflow dispatch, or release event with a release tag (aka v1.2.3).
    if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v'))

    # Create the build matrix for all the environments we're validating against.
    strategy:
      matrix:
        node-version: [ lts/-1, lts/* ]
        os: [ ubuntu-latest ]

    # Specify the environments we're going to build in.
    runs-on: ${{ matrix.os }}

    # Execute the build activities.
    steps:
      - name: Checkout the repository.
        uses: actions/checkout@v4

      - name: Setup the node ${{ matrix.node-version }} environment.
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: Build and install the package with a clean slate.
        run: |
          npm ci
          npm run build --if-present
        env:
          CI: true

  # Publish the release to the NPM registry.
  publish-npm:
    name: 'Publish package'
    needs: build

    # Publish only if we've received a release event and the tag starts with "v" (aka v1.2.3).
    if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v')

    # Specify the environment we're going to build in.
    runs-on: ubuntu-latest

    # Execute the build and publish activities.
    steps:
    - name: Checkout the repository.
      uses: actions/checkout@v4

    - name: Setup the node environment.
      uses: actions/setup-node@v4
      with:

        # Use the oldest node LTS version that we support.
        node-version: lts/-1

        # Use the NPM registry.
        registry-url: 'https://registry.npmjs.org/'

    - name: Install the package with a clean slate.
      run: npm ci

    - name: Publish the package to NPM.
      run: npm publish --access public
      env:
        NODE_AUTH_TOKEN: ${{ secrets.npm_token }}


================================================
FILE: .github/workflows/dependabot-automerge.yml
================================================
# Automerge dependency updates identified by dependabot.
#
name: Automerge Dependabot Version Updates

on:
  pull_request_target:

jobs:
  auto-merge:
    runs-on: ubuntu-latest
    if: github.actor == 'dependabot[bot]'
    steps:
      - uses: actions/checkout@v2
      - uses: ahmadnassri/action-dependabot-auto-merge@v2
        with:
          target: minor
          github-token: ${{ secrets.UPDATES_TOKEN }}


================================================
FILE: .github/workflows/issue-stale.yml
================================================
# Close stale issues after a defined period of time.
#
name: Close Stale Issues

on:
  issues:
    types: [reopened]
  schedule:
  - cron: "*/60 * * * *"

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
    - name: Autoclose stale issues.
      uses: actions/stale@v9
      with:
        days-before-close: 2
        days-before-stale: 4
        exempt-issue-labels: 'discussion,help wanted,long running'
        exempt-pr-labels: 'awaiting-approval,work-in-progress'
        remove-stale-when-updated: true
        repo-token: ${{ secrets.GITHUB_TOKEN }}
        stale-issue-label: 'stale'
        stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
        stale-pr-label: 'stale'
        stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'


================================================
FILE: .github/workflows/issue-validate.yml
================================================
# Close issues that don't conform to the issue templates.
#
name: Close Non-Conforming Issues

on:
  issues:
    types: [opened]

jobs:
  autoclose:
    runs-on: ubuntu-latest
    steps:
    - name: Autoclose issues that don't follow the issue templates.
      uses: roots/issue-closer@v1.1
      with:
        issue-close-message: "@${issue.user.login} - this issue is being automatically closed because it does not follow either the feature request or bug report issue template. The issue templates have been designed to help in the troubleshooting (or feature request) process. Please use them and populate it as completely as possible to streamline troubleshooting or feature request discussions."
        issue-pattern: "Describe alternatives you've considered|Homebridge-myQ Plugin Version"
        repo-token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/lock-threads.yml
================================================
name: 'Lock Threads'

on:
  schedule:
    - cron: '0 1 * * *'

permissions:
  issues: write
  pull-requests: write

concurrency:
  group: lock

jobs:
  action:
    runs-on: ubuntu-latest
    steps:
      - uses: dessant/lock-threads@v4
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          issue-inactive-days: "2"
          exclude-any-issue-labels: "discussion"
          issue-comment: "This issue is locked to prevent necroposting on closed issues. Please create a new issue for related support requests, bug reports, or feature suggestions."
          issue-lock-reason: ""
          pr-inactive-days: "7"
          pr-comment: "This issue is locked to prevent necroposting on closed issues. Please create a new issue for related discussion, if needed."
          pr-lock-reason: ""


================================================
FILE: .gitignore
================================================
# Ignore compiled code
dist

# Ignore npmrc.
.npmrc

# Ignore macOS attribute files.
.DS_Store

# ------------- Defaults ------------- #

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*


================================================
FILE: .npmignore
================================================
# Ignore everything by default.
*

# Include the following.
!LICENSE.md
!README.md
!config.schema.json
!dist/**
!homebridge-ui/**
!package.json


================================================
FILE: CODE-OF-CONDUCT.md
================================================
# Code of Conduct

By interacting with this GitHub repository, you agree that you'll follow this code of conduct.

### In short: Be nice. Be respectful. No harassment, trolling, or spamming.

Always be mindful that in the free / open source community, people are contributing their time away from friends and families to work on these projects. No one is being compensated for their work here. While feedback is useful, coming to this repository to make demands isn’t respectful.

* Harassment includes sexual language and imagery, deliberate intimidation, stalking, name-calling, unwelcome attention, libel, and any malicious hacking or social engineering. This repository should be a harassment-free experience for everyone, regardless of your background, identity, or experience level.

* Trolling includes posting inflammatory comments to provoke an emotional response or disrupt discussions.

* Spamming includes posting off-topic messages to disrupt discussions, promote a product, solicit donations, advertise a job / internship / gig, or flooding discussions with files or text.

#### The maintainers of this GitHub repository will take any action we deem appropriate, up to and including banning the offender from this repository.

##### Attribution
This code of conduct was inspired by and adapted from the [freeCodeCamp](https://www.freecodecamp.org/news/code-of-conduct/) code of conduct.


================================================
FILE: LICENSE.md
================================================
Internet Systems Consortium license
===================================

Copyright (c) `2017-2023`, `HJD https://github.com/hjdhjd`

Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.

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.


================================================
FILE: README.md
================================================
<SPAN ALIGN="CENTER" STYLE="text-align:center">
<DIV ALIGN="CENTER" STYLE="text-align:center">

[![homebridge-myq: Native HomeKit support for myQ garage door openers and other devices](https://raw.githubusercontent.com/hjdhjd/homebridge-myq/main/homebridge-myq.svg)](https://github.com/hjdhjd/homebridge-myq)

# Homebridge myQ

[![Downloads](https://img.shields.io/npm/dt/homebridge-myq?color=%235EB5E5&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/homebridge-myq)
[![Version](https://img.shields.io/npm/v/homebridge-myq?color=%235EB5E5&label=Homebridge%20myQ&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/homebridge-myq)
[![myQ@Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=%235EB5E5&label=Discord&logo=discord&logoColor=%23FFFFFF&style=for-the-badge)](https://discord.gg/QXqfHEW)
[![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%2357277C&style=for-the-badge&logoColor=%23FFFFFF&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5OTIuMDkiIGhlaWdodD0iMTAwMCIgdmlld0JveD0iMCAwIDk5Mi4wOSAxMDAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYSIgZD0iTTk1MC4xOSw1MDguMDZhNDEuOTEsNDEuOTEsMCwwLDEtNDItNDEuOWMwLS40OC4zLS45MS4zLTEuNDJMODI1Ljg2LDM4Mi4xYTc0LjI2LDc0LjI2LDAsMCwxLTIxLjUxLTUyVjEzOC4yMmExNi4xMywxNi4xMywwLDAsMC0xNi4wOS0xNkg3MzYuNGExNi4xLDE2LjEsMCwwLDAtMTYsMTZWMjc0Ljg4bC0yMjAuMDktMjEzYTE2LjA4LDE2LjA4LDAsMCwwLTIyLjY0LjE5TDYyLjM0LDQ3Ny4zNGExNiwxNiwwLDAsMCwwLDIyLjY1bDM5LjM5LDM5LjQ5YTE2LjE4LDE2LjE4LDAsMCwwLDIyLjY0LDBMNDQzLjUyLDIyNS4wOWE3My43Miw3My43MiwwLDAsMSwxMDMuNjIuNDVMODYwLDUzOC4zOGE3My42MSw3My42MSwwLDAsMSwwLDEwNGwtMzguNDYsMzguNDdhNzMuODcsNzMuODcsMCwwLDEtMTAzLjIyLjc1TDQ5OC43OSw0NjguMjhhMTYuMDUsMTYuMDUsMCwwLDAtMjIuNjUuMjJMMjY1LjMsNjgwLjI5YTE2LjEzLDE2LjEzLDAsMCwwLDAsMjIuNjZsMzguOTIsMzlhMTYuMDYsMTYuMDYsMCwwLDAsMjIuNjUsMGwxMTQtMTEyLjM5YTczLjc1LDczLjc1LDAsMCwxLDEwMy4yMiwwbDExMywxMTEsLjQyLjQyYTczLjU0LDczLjU0LDAsMCwxLDAsMTA0TDU0NS4wOCw5NTcuMzV2LjcxYTQxLjk1LDQxLjk1LDAsMSwxLTQyLTQxLjk0Yy41MywwLC45NS4zLDEuNDQuM0w2MTYuNDMsODA0LjIzYTE2LjA5LDE2LjA5LDAsMCwwLDQuNzEtMTEuMzMsMTUuODUsMTUuODUsMCwwLDAtNC43OS0xMS4zMmwtMTEzLTExMWExNi4xMywxNi4xMywwLDAsMC0yMi42NiwwTDM2Ny4xNiw3ODIuNzlhNzMuNjYsNzMuNjYsMCwwLDEtMTAzLjY3LS4yN2wtMzktMzlhNzMuNjYsNzMuNjYsMCwwLDEsMC0xMDMuODZMNDM1LjE3LDQyNy44OGE3My43OSw3My43OSwwLDAsMSwxMDMuMzctLjlMNzU4LjEsNjM5Ljc1YTE2LjEzLDE2LjEzLDAsMCwwLDIyLjY2LDBsMzguNDMtMzguNDNhMTYuMTMsMTYuMTMsMCwwLDAsMC0yMi42Nkw1MDYuNSwyNjUuOTNhMTYuMTEsMTYuMTEsMCwwLDAtMjIuNjYsMEwxNjQuNjksNTgwLjQ0QTczLjY5LDczLjY5LDAsMCwxLDYxLjEsNTgwTDIxLjU3LDU0MC42OWwtLjExLS4xMmE3My40Niw3My40NiwwLDAsMSwuMTEtMTAzLjg4TDQzNi44NSwyMS40MUE3My44OSw3My44OSwwLDAsMSw1NDAsMjAuNTZMNjYyLjYzLDEzOS4zMnYtMS4xYTczLjYxLDczLjYxLDAsMCwxLDczLjU0LTczLjVINzg4YTczLjYxLDczLjYxLDAsMCwxLDczLjUsNzMuNVYzMjkuODFhMTYsMTYsMCwwLDAsNC43MSwxMS4zMmw4My4wNyw4My4wNWguNzlhNDEuOTQsNDEuOTQsMCwwLDEsLjA4LDgzLjg4WiIvPjwvc3ZnPg==)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)

## myQ garage door and other myQ-enabled device support for [Homebridge](https://homebridge.io).
</DIV>
</SPAN>

### `homebridge-myq` is officially retired, for now. For those interested in an alternative solution, I would highly recommend looking into [Ratgdo](https://paulwieland.github.io/ratgdo/) and my [homebridge-ratgdo](https://github.com/hjdhjd/homebridge-ratgdo) plugin that provides all the capabilities of `homebridge-myq` and more capabilities that were never possible due to the myQ API constraints. Thank you for all the support and to the members of the community I've gotten to know over the years.

`homebridge-myq` ~~is~~was a [Homebridge](https://homebridge.io) plugin that makes myQ-enabled devices available to [Apple's](https://www.apple.com) [HomeKit](https://www.apple.com/ios/home) smart home platform. myQ-enabled devices include many smart garage door openers made primarily by Liftmaster, Chamberlain, and Craftsman, but includes other brands as well. You can determine if your garage door or other device is myQ-enabled by checking the [myQ compatibility check tool](https://www.myq.com/myq-compatibility) on the myQ website.

There ~~are~~were two ways to control a myQ-compatible garage door opener through [HomeKit](https://www.apple.com/ios/home):

1. Liftmaster and Chamberlain make a hardware HomeKit bridge also called [Home Bridge](https://www.liftmaster.com/myq-home-bridge/p/G819LMB) (not to be confused with the open source [Homebridge project](https://homebridge.io)).
Unfortunately, some of us have encountered significant issues with the hardware bridge in a real world setting, where it either stops working or hangs for extended periods of time. That said, other users have encountered no issues and this hardware solution works well.

2. A plugin for [Homebridge](https://homebridge.io) like this one that emulates the capabilities of a myQ [HomeKit](https://www.apple.com/ios/home) bridge device.

Either solution will provide you with robust HomeKit integration, and you'll soon be automating your myQ smart garage with the richness of Apple's HomeKit ecosystem!

## Why use this plugin for myQ support in HomeKit?
In a nutshell, the aim of this plugin for things to *just work* with minimal required configuration by users. The goal is to provide as close to a streamlined experience as you would expect from a first-party or native HomeKit solution. For the adventurous, those additional granular options are, of course, available to support more esoteric use cases or other unique needs.

What does *just work* mean in practice? It means that this plugin will discover all your myQ devices and poll at regular, reasonable intervals for changes in state of a garage door opener, lamp, or other myQ devices and inform HomeKit of those changes. By default. Without additional configuration beyond the login information required for myQ services.

`homebridge-myq` has been around a long time and is trusted by thousands of users. It's the first myQ [Homebridge](https://homebridge.io) plugin to provide comprehensive support for various myQ features such as obstruction detection, and the first to provide support for multiple myQ device types. As more of the API can be decoded, my aim is to support as many device types as possible. I rely on this plugin every day and actively maintain and support it.

### Features
- ***Easy* configuration - all you need is your myQ username and password to get started.** The defaults work for the vast majority of users. When you want more, there are [advanced options](#advanced-config) you can play with, if you choose.

- **Automatic detection and configuration of all lamps, garage door and gate openers.** By default - all of your supported myQ devices are made available in HomeKit.

- **[Obstruction detection](#obstruction-status) on supported myQ garage door and gate openers.** When a garage door or gate is obstructed, and the myQ API provides that information, you'll see an alert raised in the Home app.

- **[Battery status detection](#battery-status) on supported myQ door position sensor devices.** If you have a myQ supported door position sensor, you'll see an alert raised in the Home app to inform you when the battery is running low.

- **The ability to [selectively hide and show](#feature-options) specific myQ devices.** For those who only want to show particular devices in HomeKit, or particular homes, a flexible and intuitive way to configure device availability at a granular level is available.

- **Full MQTT support.** For those who use MQTT, this plugin provides full MQTT support with a rich set of options. [Read the MQTT documentation](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/MQTT.md) for more details.

### <A NAME="myq-contribute"></A>How you can contribute and make this plugin even better
The myQ API is undocumented and implementing a plugin like this one is the result of many hours of reverse engineering, trial and error, and community support. This work stands on the shoulders of other myQ API projects out there and this project attempts to contribute back to that community base of knowledge to further improve myQ support for everyone.

I would love to support more types of myQ devices. Currently `homebridge-myq` supports the following device types:

- Garage door and gate openers
- Lamps and myQ switches

Additional device types will be added as time allows. It's unlikely I will add support for myQ locks due to their deprecated protocol support. myQ cameras may be added at some point, but there are additional non-technical considerations related to the paid myQ product that I want to be mindful of.

## Documentation
* Getting Started
  * [Installation](#installation): installing this plugin, including system requirements.
  * [Plugin Configuration](#plugin-configuration): how to quickly get up and running.
  * [Additional Notes](#notes): some things you should be aware of, including myQ-specific quirks.

* Advanced Topics
  * [Feature Options](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/FeatureOptions.md): granular options to allow you to show or hide specific garage door openers and more.
  * [MQTT](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/MQTT.md): how to configure MQTT support.
  * [Advanced Configuration](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/AdvancedOptions.md): complete list of configuration options available in this plugin.
  * [Changelog](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/Changelog.md): changes and release history of this plugin, starting with v2.0.

## Installation
If you are new to Homebridge, please first read the [Homebridge](https://homebridge.io) [documentation](https://github.com/homebridge/homebridge/wiki) and installation instructions before proceeding.

If you have installed the [Homebridge Config UI](https://github.com/homebridge/homebridge-config-ui-x), you can intall this plugin by going to the `Plugins` tab and searching for `homebridge-myq` and installing it.

### <A NAME="notes"></A>Things To Be Aware Of
- <A NAME="myq-errors"></A>The myQ API has largely stabilized in recent years, particularly with the advent of the v6 version of the API. However, the myQ cloud can (and does) have occasional reliability challenges that can manifest through connectivity issues that you will see in the Homebridge logs related to this plugin. These are not bugs in `homebridge-myq` and there's not much we can do it aside from attempt to gracefully handle the errors when they occur. The myQ cloud is quite reliable the large majority (~93-95% by my estimate) of the time. Please do not open bug reports related to API issues - they'll be closed summarily unless it's a new API-specific issue that's emerged as a result of changes to the undocumented myQ API. These issues are largely harmless and resolve themselves on their own, usually in minutes, and occasionally in hours or days.

- **As a result of the above you *will* see errors similar to this on an occasional basis in the Homebridge logs:**

    ```
    myQ API: Unable to update device status from the myQ API. Acquiring a new access token.
    ```
  These messages can be safely ignored. myQ API errors *will* inevtiably happen. The myQ cloud infrastructure from Liftmaster / Chamberlain is not completely reliable and occasionally errors out due to infrastructure maintenance, network issues, or other infrastructure hiccups that occur on the myQ end of things. This plugin has no control over this. The logging is informative and **not a cause for significant concern unless it is constant and ongoing for multiple days**, which would be indicative of the larger API issues referenced above. When one of these errors is detected, we log back into the myQ infrastructure, obtain new API security credentials, and attempt refresh our status in the next scheduled update, which by is roughly [every 12 seconds by default](#advanced-config).

- <A NAME="obstruction-status"></A>Obstruction detection in myQ is more nuanced than one might think at first glance. When myQ detects an obstruction, that obstruction is only visible in the API for a *very* small amount of time, typically no more than a few seconds. This presents a user experience problem - if you remain completely faithful to the myQ API and only show the user the obstruction for the very short amount of time that it actually occurs, the user might never notice it because the alert is not visible for more than a few seconds. Instead, the design decision I've chosen to make is to ensure that any detected obstruction is alerted in HomeKit for 30 seconds from the last time myQ detected that obstruction. This ensures that the user has a reasonable chance of noticing there was an obstruction at some point in the very recent past, without having to have the user stare at the Home app constantly to happen to catch an ephemeral state.

## Plugin Configuration
If you choose to configure this plugin directly instead of using the [Homebridge Configuration web UI](https://github.com/oznu/homebridge-config-ui-x), you'll need to add the platform to your `config.json` in your home directory inside `.homebridge`.

```js
"platforms": [{
    "platform": "myQ",
    "email": "email@email.com",
    "password": "password"
}]
```

For most people, I recommend using [Homebridge Configuration web UI](https://github.com/oznu/homebridge-config-ui-x) to configure this plugin rather than doing so directly. It's easier to use for most users, especially newer users, and less prone to typos, leading to other problems.

## Plugin Development Dashboard
This is mostly of interest to the true developer nerds amongst us.

[![License](https://img.shields.io/npm/l/homebridge-myq?color=%230559C9&logo=open%20source%20initiative&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/homebridge-myq/blob/main/LICENSE.md)
[![Build Status](https://img.shields.io/github/actions/workflow/status/hjdhjd/homebridge-myq/ci.yml?branch=main&color=%230559C9&logo=github-actions&logoColor=%23FFFFFF&style=for-the-badge)](https://github.com/hjdhjd/homebridge-myq/actions?query=workflow%3A%22Continuous+Integration%22)
[![Dependencies](https://img.shields.io/librariesio/release/npm/homebridge-myq?color=%230559C9&logo=dependabot&style=for-the-badge)](https://libraries.io/npm/homebridge-myq)
[![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/hjdhjd/homebridge-myq/latest?color=%230559C9&logo=github&sort=semver&style=for-the-badge)](https://github.com/hjdhjd/homebridge-myq/commits/main)


================================================
FILE: config.schema.json
================================================
{
  "pluginAlias": "myQ",
  "pluginType": "platform",
  "singular": true,
  "customUi": true,
  "headerDisplay": "[homebridge-myq](https://github.com/hjdhjd/homebridge-myq) provides HomeKit support to myQ-enabled smart garage door openers and other devices.",
  "footerDisplay": "See the [homebridge-myq developer page](https://github.com/hjdhjd/homebridge-myq) for detailed documentation, including [feature options](https://github.com/hjdhjd/homebridge-myq#feature-options).",
  "schema": {
    "type": "object",
    "properties": {

      "email": {
        "title": "myQ Email",
        "type": "string",
        "required": true,
        "placeholder": "user@example.com",
        "description": "Email address used for your myQ account.",
        "x-schema-form": {
           "type": "email"
         }
      },

      "password": {
        "title": "myQ Password",
        "type": "string",
        "required": true,
        "placeholder": "mypassword",
        "description": "Password used for your myQ account.",
        "x-schema-form": {
           "type": "password"
         }
      },

      "name": {
        "title": "Plugin Name",
        "type": "string",
        "required": true,
        "default": "myQ",
        "description": "Name to use for Homebridge logging purposes. Default: myQ."
      },

      "options": {
        "title": "Feature Options",
        "type": "array",

        "items": {
          "type": "string",
          "title": "Feature Option",
          "required": false,
          "description": "Enter only one option per entry. See the plugin documentation for the complete list of available options or use the feature options webUI tab above.",
          "placeholder": "e.g. Disable.Device.SerialNumber"
        }
      },

      "mqttTopic": {
        "type": "string",
        "title": "MQTT Base Topic",
        "required": false,
        "placeholder": "e.g. myq",
        "description": "The base MQTT topic to publish to. Default: myq."
      },

      "mqttUrl": {
        "type": "string",
        "title": "MQTT Broker URL",
        "required": false,
        "format": "uri",
        "placeholder": "e.g. mqtt://1.2.3.4",
        "description": "URL for the MQTT broker you'd like to publish event messages to. Default: None."
      },

      "refreshInterval": {
        "title": "Refresh Interval",
        "type": "integer",
        "minimum": 5,
        "maximum": 60,
        "required": false,
        "description": "Normal myQ status refresh interval, in seconds. Default: 12."
      },

      "activeRefreshInterval": {
        "title": "Active Refresh Interval",
        "type": "integer",
        "minimum": 2,
        "maximum": 10,
        "required": false,
        "description": "Refresh interval in seconds to use once device state changes are detected. Default: 3."
      },

      "activeRefreshDuration": {
        "title": "Active Refresh Duration",
        "minimum": 5,
        "maximum": 900,
        "type": "integer",
        "required": false,
        "description": "Duration in seconds to use the Active Refresh Interval to query for additional device state changes. Default: 300."
      },

      "debug": {
        "title": "Debug Logging",
        "type": "boolean",
        "required": false,
        "description": "Logging verbosity for debugging. Default: false."
      }

    }
  },

  "layout": [
    {
      "type": "section",
      "title": "myQ Login Credentials",
      "expandable": true,
      "expanded": false,
      "items": [
        {
          "description": "Enter your myQ email and password below.",
          "items": [
            "email",
            "password"
          ]
        }
      ]
    },

    {
      "type": "section",
      "title": "Plugin Feature Options (Optional)",
      "expandable": true,
      "expanded": false,
      "items": [
        {
          "key": "options",
          "type": "array",
          "orderable": true,
          "title": " ",
          "description": "Use the feature options webUI tab above instead of manually configuring feature options here.",
          "buttonText": "Add Feature Option",
          "items": [
            "options[]"
          ]
        }
      ]
    },

    {
      "type": "section",
      "title": "MQTT Settings (Optional)",
      "expandable": true,
      "expanded": false,
      "items": [
        {
          "description": "MQTT support will only be enabled if an MQTT broker URL is specified below.",
          "items": [
            "mqttUrl",
            "mqttTopic"
          ]
        }
      ]
    },

    {
      "type": "section",
      "title": "Advanced Settings (Optional)",
      "expandable": true,
      "expanded": false,
      "items": [
        {
          "description": "These settings should be rarely used or needed by most people. Use these with caution.",
          "items": [
            "name",
            "refreshInterval",
            "activeRefreshInterval",
            "activeRefreshDuration",
            "debug"
          ]
        }
      ]
    }

  ]
}


================================================
FILE: docs/AdvancedOptions.md
================================================
<SPAN ALIGN="CENTER" STYLE="text-align:center">
<DIV ALIGN="CENTER" STYLE="text-align:center">

[![homebridge-myq: Native HomeKit support for myQ garage door openers and other devices](https://raw.githubusercontent.com/hjdhjd/homebridge-myq/main/homebridge-myq.svg)](https://github.com/hjdhjd/homebridge-myq)

# Homebridge myQ

[![Downloads](https://img.shields.io/npm/dt/homebridge-myq?color=%235EB5E5&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/homebridge-myq)
[![Version](https://img.shields.io/npm/v/homebridge-myq?color=%235EB5E5&label=Homebridge%20myQ&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/homebridge-myq)
[![myQ@Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=%235EB5E5&label=Discord&logo=discord&logoColor=%23FFFFFF&style=for-the-badge)](https://discord.gg/QXqfHEW)
[![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%2357277C&style=for-the-badge&logoColor=%23FFFFFF&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5OTIuMDkiIGhlaWdodD0iMTAwMCIgdmlld0JveD0iMCAwIDk5Mi4wOSAxMDAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYSIgZD0iTTk1MC4xOSw1MDguMDZhNDEuOTEsNDEuOTEsMCwwLDEtNDItNDEuOWMwLS40OC4zLS45MS4zLTEuNDJMODI1Ljg2LDM4Mi4xYTc0LjI2LDc0LjI2LDAsMCwxLTIxLjUxLTUyVjEzOC4yMmExNi4xMywxNi4xMywwLDAsMC0xNi4wOS0xNkg3MzYuNGExNi4xLDE2LjEsMCwwLDAtMTYsMTZWMjc0Ljg4bC0yMjAuMDktMjEzYTE2LjA4LDE2LjA4LDAsMCwwLTIyLjY0LjE5TDYyLjM0LDQ3Ny4zNGExNiwxNiwwLDAsMCwwLDIyLjY1bDM5LjM5LDM5LjQ5YTE2LjE4LDE2LjE4LDAsMCwwLDIyLjY0LDBMNDQzLjUyLDIyNS4wOWE3My43Miw3My43MiwwLDAsMSwxMDMuNjIuNDVMODYwLDUzOC4zOGE3My42MSw3My42MSwwLDAsMSwwLDEwNGwtMzguNDYsMzguNDdhNzMuODcsNzMuODcsMCwwLDEtMTAzLjIyLjc1TDQ5OC43OSw0NjguMjhhMTYuMDUsMTYuMDUsMCwwLDAtMjIuNjUuMjJMMjY1LjMsNjgwLjI5YTE2LjEzLDE2LjEzLDAsMCwwLDAsMjIuNjZsMzguOTIsMzlhMTYuMDYsMTYuMDYsMCwwLDAsMjIuNjUsMGwxMTQtMTEyLjM5YTczLjc1LDczLjc1LDAsMCwxLDEwMy4yMiwwbDExMywxMTEsLjQyLjQyYTczLjU0LDczLjU0LDAsMCwxLDAsMTA0TDU0NS4wOCw5NTcuMzV2LjcxYTQxLjk1LDQxLjk1LDAsMSwxLTQyLTQxLjk0Yy41MywwLC45NS4zLDEuNDQuM0w2MTYuNDMsODA0LjIzYTE2LjA5LDE2LjA5LDAsMCwwLDQuNzEtMTEuMzMsMTUuODUsMTUuODUsMCwwLDAtNC43OS0xMS4zMmwtMTEzLTExMWExNi4xMywxNi4xMywwLDAsMC0yMi42NiwwTDM2Ny4xNiw3ODIuNzlhNzMuNjYsNzMuNjYsMCwwLDEtMTAzLjY3LS4yN2wtMzktMzlhNzMuNjYsNzMuNjYsMCwwLDEsMC0xMDMuODZMNDM1LjE3LDQyNy44OGE3My43OSw3My43OSwwLDAsMSwxMDMuMzctLjlMNzU4LjEsNjM5Ljc1YTE2LjEzLDE2LjEzLDAsMCwwLDIyLjY2LDBsMzguNDMtMzguNDNhMTYuMTMsMTYuMTMsMCwwLDAsMC0yMi42Nkw1MDYuNSwyNjUuOTNhMTYuMTEsMTYuMTEsMCwwLDAtMjIuNjYsMEwxNjQuNjksNTgwLjQ0QTczLjY5LDczLjY5LDAsMCwxLDYxLjEsNTgwTDIxLjU3LDU0MC42OWwtLjExLS4xMmE3My40Niw3My40NiwwLDAsMSwuMTEtMTAzLjg4TDQzNi44NSwyMS40MUE3My44OSw3My44OSwwLDAsMSw1NDAsMjAuNTZMNjYyLjYzLDEzOS4zMnYtMS4xYTczLjYxLDczLjYxLDAsMCwxLDczLjU0LTczLjVINzg4YTczLjYxLDczLjYxLDAsMCwxLDczLjUsNzMuNVYzMjkuODFhMTYsMTYsMCwwLDAsNC43MSwxMS4zMmw4My4wNyw4My4wNWguNzlhNDEuOTQsNDEuOTQsMCwwLDEsLjA4LDgzLjg4WiIvPjwvc3ZnPg==)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)

## myQ garage door and other myQ-enabled device support for [Homebridge](https://homebridge.io).
</DIV>
</SPAN>

`homebridge-myq` is a [Homebridge](https://homebridge.io) plugin that makes myQ-enabled devices available to [Apple's](https://www.apple.com) [HomeKit](https://www.apple.com/ios/home) smart home platform. myQ-enabled devices include many smart garage door openers made primarily by Liftmaster, Chamberlain, and Craftsman, but includes other brands as well. You can determine if your garage door or other device is myQ-enabled by checking the [myQ compatibility check tool](https://www.myq.com/myq-compatibility) on the myQ website.

### Advanced Configuration (Optional)
This step is not required. The defaults should work well for almost everyone, but for those that prefer to tweak additional settings, this is the complete list of settings available.

```js
"platforms": [
  {
    "platform": "myQ",
    "name": "myQ",
    "email": "email@email.com",
    "password": "password",
    "refreshInterval": 12,
    "activeRefreshInterval": 3,
    "activeRefreshDuration": 300,
    "options": ["Disable.Device.CG12345", "Enable.Device.CG6789"],
    "mqttUrl": "mqtt:1.2.3.4",
    "mqttTopic": "myq",
    "userAgent": "xyzxyz",
    "debug": false
  }
]
```

| Fields                | Description                                                                        | Default | Required |
|-----------------------|------------------------------------------------------------------------------------|---------|----------|
| platform              | Must always be `myQ`.                                                              |         | Yes      |
| name                  | For logging purposes.                                                              |         | No       |
| email                 | Your myQ account email.                                                            |         | Yes      |
| password              | Your myQ account password.                                                         |         | Yes      |
| refreshInterval       | Normal myQ device refresh interval in `seconds`.                                   | 12      | No       |
| activeRefreshInterval | Refresh interval in `seconds` to use when myQ device state changes are detected.   | 3       | No       |
| activeRefreshDuration | Duration in `seconds` to use `activeRefreshInterval` to refresh myQ device status. | 300     | No       |
| options               | Configure plugin [feature options](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/FeatureOptions.md).  | []      | No       |
| mqttUrl               | The URL of your MQTT broker. **This must be in URL form**, e.g.: `mqtt://user@password:1.2.3.4`. |      | No       |
| mqttTopic             | The base topic to use when publishing MQTT messages.                               | "myq"   | No       |
| userAgent             | Override the builtin randomly generated user agent. **Use with extreme care.**   | Randomly generated by `homebridge-myq`   | No       |
| debug                 | Logging verbosity for debugging purporses.                                         | false   | No       |


================================================
FILE: docs/Changelog.md
================================================
# Changelog

All notable changes to this project will be documented in this file. This project uses [semantic versioning](https://semver.org/).

## 3.4.4 (2024-04-28)
  * Retirement announcement. `homebridge-myq` is officially retired, for now. For those interested in an alternative solution, I would highly recommend looking into [Ratgdo](https://paulwieland.github.io/ratgdo/) and my [homebridge-ratgdo](https://github.com/hjdhjd/homebridge-ratgdo) plugin that provides all the capabilities of `homebridge-myq` and more capabilities that were never possible due to the myQ API constraints. Thank you for all the support and to the members of the community I've gotten to know over the years.

## 3.4.3 (2023-10-15)
  * Improvement: further enhancements to the robustness of our myQ connectivity.

## 3.4.2 (2023-10-12)
  * Fix: myQ login shenanigans. Looks like myQ is starting to get a lot more strict on API access - let's see how things go.

## 3.4.1 (2023-09-17)
  * Fix: address lamp regressions.
  * Housekeeping.

## 3.4.0 (2023-09-15)
  * Improvement: to improve resilience, the API will automatically retry across regions when myQ API issues are encountered. The `region` advanced setting is no longer available and has been removed. HBMQ will automatically retry the API until it finds a path that works.
  * Housekeeping.

## 3.3.0 (2023-08-27)
  * Improvement: new first run user experience in the webUI for new users, improved API error reporting in the webUI.
  * Housekeeping.

## 3.2.1 (2023-08-22)
  * Fix: address MQTT regressions.
  * Housekeeping.

## 3.2.0 (2023-08-21)
  * New feature: automation switch support. This feature is intended for automation scenarios where you have a need to bypass the very sensible security precautions HomeKit has placed on how you can automate the opening and closing of garage door openers. This feature will allow you to control the garage door opener through a switch accessory. Disabled by default, and configurable in the myQ webUI.
  * Improvement: further refinements to the webUI and first run experience.
  * Housekeeping.

## 3.1.0 (2023-08-20)
  * New feature: open state occupancy sensor support. This is a useful feature to those who want to create automations based on the opener being **open** for an extended duration of time. By default, the duration is 5 minutes, but it is configurable within the myQ webUI. See the feature option tab for all the goodies. Disabled by default.
  * Improvement: commands to offline myQ garage door openers are more gracefully handled.

## 3.0.0 (2023-08-19)
  * New feature: updated and modernized webUI to be inline with my other plugins. **Please note: there are several breaking changes to feature option names in this release. Please use the webUI to configure feature options.** For those familiar with one of my other plugins, `homebridge-unifi-protect` the webUI will look quite familiar to you.
  * New feature: you can now synchronize names of your myQ devices with HomeKit. Synchronization is one-way and it will always view the myQ name as the source. The option is disabled by default.
  * Improvement: further refinements to the myQ API to handle some of the more common errors gracefully and be more forgiving of issues on the myQ cloud end of the equation.
  * **Note as of v3.0.0, Node 18 is the minimum required version of Node for `homebridge-myq`.**
  * Housekeeping and documentation improvements.

## 2.12.0 (2023-05-14)
  * Housekeeping.

## 2.11.1 (2023-04-13)
## 2.11.0 (2023-04-13)
  * Feature: allow for user-selectable cloud geographic regions for the myQ API.

## 2.10.2 (2023-04-11)
## 2.10.1 (2023-04-11)
  * Fix: webUI bugfixes.
  * Housekeeping.

## 2.10.0 (2023-04-10)
  * Improvement: better performance across the board.
  * Housekeeping.

## 2.9.0 (2023-01-03)
  * Feature: new feature option to allow users to disable battery notifications for myQ devices with a battery-enabled door position sensor. You can disable low battery notifications by using `Disable.BatteryInfo`.
  * Feature: new feature option to allow for setting garage door openers to read-only. You can configure garage door openers as read-only using `Enable.ReadOnly`.

## 2.8.3 (2022-12-27)
  * More housekeeping.

## 2.8.2 (2022-12-27)
  * Dependency updates and housekeeping.

## 2.8.1 (2022-12-05)
  * Web UI to peruse through the list of detected myQ devices.
  * Dependency updates and housekeeping.

## 2.8.0 (2022-12-05)
  * Web UI to peruse through the list of detected myQ devices.
  * Dependency updates and housekeeping.

## 2.7.4 (2022-01-17)
  * Dependency updates.

## 2.7.3 (2022-01-09)
  * Housekeeping.

## 2.7.2 (2022-01-01)
  * Lock `mqtt` upstream package version due to a bug introduced in a newer version until it gets sorted out.
  * Dependency updates.

## 2.7.1 (2021-09-18)
  * A housekeeping release to remove the core myQ API library out of this plugin and into it's own package to make it available to other developers who want to support myQ capabilities in their projects.

## 2.7.0 (2021-09-17)
  * Refine how myQ API access tokens refresh credentials in the v6 API.
  * Refine how we handle connection resets by retrying a reset connection before abandoning it and logging into the API anew.

## 2.6.5 (2021-08-17)
  * Fix for log warnings generated by models of myQ openers not currently known to the plugin.

## 2.6.4 (2021-08-16)
  * Improve response checking from the myQ API on logins.
  * Housekeeping.

## 2.6.3 (2021-07-19)
  * Dependency updates.

## 2.6.2 (2021-01-31)
  * Fix: lamps should now work under the new myQ API.

## 2.6.1 (2021-01-23)
  * Re-release for housekeeping reasons.
  * Feature: guest accounts are now formally supported. Your myQ credentials will now show all devices your login has access to.
  * Change: **feature option semantics have changed** to be consistent across the other plugins I develop and maintain. What used to be: `Hide.serialnumber` and `Show.serialnumber` are now `Disable.serialnumber` and `Enable.serialnumber`.

  * Updates from v2.5.0:
    * Feature: support for myQ API v6 allowing `homebridge-myq` to use the latest features and evolving capabilities of the myQ API. This should help deal with the deprecation of the legacy login methods that Liftmaster / Chamberlain are clearly in the process of retiring. A huge thank you to @jarz who partnered with me on this one...he did most of the real heavy lifting in getting a working OAuth+PKCE-based login proof-of-concept working. Thank you for your contribution to the community and your partnership. Some things of note with the new API:

      * Looks like we have better potential access to cameras and locks...more exploration is needed...nothing to report for now.
      * **myQ lamp devices may or may not work**. I didn't have a lamp device to test with, and had to take an educated guess at how to execute lamp-related commands in the new API. **I would welcome reports on this for lamp users to see whether things work or not.**
    * Removed legacy options that aren't relevant with the myQ v6 API.
    * Housekeeping and dependency updates.

## 2.4.2 (2021-01-13)
  * Deal with the latest minor updates in the myQ API as an interim step while we deal with the larger myQ API version update. More work ahead, but this should get most people back up and running - for now.
  * Dependency updates.

## 2.4.1 (2021-01-01)
  * Housekeeping and dependency updates.

## 2.4.0 (2020-12-21)
  * Feature: future-proofing against potential API issues allowing users to override the user agent, when needed.

## 2.3.7 (2020-12-18)
  * myQ API changes broke things...again...so we fix them...again! The initial login request changed ever so slightly.

## 2.3.6 (2020-12-16)
  * myQ API changes broke things...so we fix them! `homebridge-myq` will now generate a random user agent string at startup to avoid potential API blacklisting by myQ servers.

## 2.3.5 (2020-11-22)
  * Housekeeping.

## 2.3.4 (2020-11-22)
  * Dependency updates.

## 2.3.3 (2020-11-01)
  * Workaround for occasional HomeKit quirks in garage door notifications. This should result in more robust (and less quirky) notification behavior when you open and close garage doors.
  * Minor housekeeping and dependency updates.

## 2.3.2 (2020-10-11)
  * Small optimization around Homebridge startup.
  * Dependency updates.

## 2.3.1 (2020-09-27)
  * Logging refinements and housekeeping.

## 2.3.0 (2020-09-19)
  * Support for myQ-enabled lights, lamps, and switches.

## 2.2.2 (2020-09-16)
  * Support self-signed TLS certificates for MQTT brokers.

## 2.2.1 (2020-09-14)
  * Feature: MQTT support. Read the [MQTT documentation](https://github.com/hjdhjd/homebridge-myq/blob/main/docs/MQTT.md) for more information.
  * Improved responsiveness when we know certain events are happening.
  * Housekeeping and general improvements.

## 2.1.12 (2020-09-08)
  * **IMPORTANT: NAME CHANGE.** Starting with this release, this plugin is now renamed to `homebridge-myq`. My thanks to the previous owner of the NPM name for `homebridge-myq` for graciously transitioning it to me. What does this mean for you?
    * You should uninstall this package and reinstall it under it's new name, `homebridge-myq`. That should do the trick. Your configuration won't be impacted. Apologies for any extra gymnastics this might cause some people, but it will help future users and make this plugin more discoverable.
    * `homebridge-myq2` will soon be deprecated. You'll receive a warning message that the package has been deprecated and to install `homebridge-myq` instead.
    * Again my apologies for any extra work this causes people, but I hope it will be a mostly painless transition.
    * For those using the Homebridge webUI, it's as simple as uninstalling `homebridge-myq2` and then installing `homebridge-myq`.
    * Quick steps for those using the command line:
      ```sh
      npm -g uninstall homebridge-myq2
      npm -g install homebridge-myq
      ```
      Restart homebridge and you're all set.

  * Minor housekeeping around the name change and to prepare for the future.

## 2.1.11 (2020-08-18)
  * Enhancement: webUI updates to better support HOOBS and similar solutions.

## 2.1.10 (2020-08-14)
  * Fix: regression introduced in 2.1.9 for myQ API connectivity.

## 2.1.9 (2020-08-12)
  * Enhancement: summarize some of the more common myQ API errors so they're less intimidating in the logs.
  * Simplify the plugin webUI configuration page.
  * Housekeeping and cleanup of the codebase.
  * Update support libraries used.

## 2.1.8 (2020-07-25)
  * Additional web UI refinements.

## 2.1.6, v2.1.7 (2020-07-25)
  * Fix: remove spurious and noisy log entry when polling the myQ API.

## 2.1.5 (2020-07-25)
  * **Wanted: more myQ device types. If you have a myQ light or other non-door myQ accessory, I'd love to [hear from you](https://github.com/hjdhjd/homebridge-myq#myq-contribute) and see if we can add support for it in `homebridge-myq`.**
  * Enhancement: alert on obstructions for a longer window of time (30 seconds), to give users a better chance of noticing them. See [here](https://github.com/hjdhjd/homebridge-myq#obstruction-status) if you'd like to read more about it.
  * Enhancement: refine stopped state to better inform users when the state occurs.<BR>*Note: the default iOS Home app doesn't seem to correctly show a garage door in a stopped state, however other HomeKit apps such as Eve Home will correctly show this state when it occurs.*
  * Enhancement: increase our update resolution when we detect any state change, not just when we initiate one.
  * Enhancement: tweaked default myQ API refresh intervals to provide more frequent state updates, within reason.
  * Change: [some options have been renamed](https://github.com/hjdhjd/homebridge-myq#advanced-config), and one new option has been added - the ability to override the builtin myQ application identifier. **Use with extreme caution and only if you know what you are doing.**
  * Update debug logging approach across the codebase.
  * Miscellaneous housekeeping edits and code maintenance.
  * Updated documentation and logo.

## 2.1.4 (2020-07-20)
  * Fix: address a race condition in updating device status.
  * Logging is more standardized and refined across the plugin.
  * Deprecated and removed `openDuration` and `closeDuration`. The values have been unused internally for some time, and they don't materially impact polling resolution.
  * Prepare the plumbing for additional myQ devices.

## 2.1.3 (2020-07-17)

  * Fix: refresh security tokens more often to address potential myQ API issues.
  * Get a status update from myQ immediately on startup.
  * Remove reachability support since it's now deprecated in HomeKit and homebridge.
  * Refine logging to clarify messages and streamline in places.
  * Minor updates to the code base.

## 2.1.2 (2020-07-12)

  * Fix: repair npm install script.

## 2.1.1 (2020-07-12)

  * Enhancement: deduce the type of device and brand based on serial number.
  * Enhancement: inform users when we choose not to add a device to HomeKit because we don't support it yet.
  * Fix: don't attempt to open or close the door if we're already in that state.
  * Fix: acquire a new myQ API security token regularly (thanks @dxdc for helping track this one down).
  * Fix: address a potential race condition when we check for battery information availability (on supported models).

## 2.1.0 (2020-07-12)

  * Feature: include battery status information for devices that support it.
  * Code cleanup.

## 2.0.13 (2020-07-11)
## 2.0.12 (2020-07-11)

  * Fix: look at the `device_family` attribute to determine whether it's a garage opener or not, rather than the `device_type` attribute.

## 2.0.11

  * New feature: feature options. This replaces the previous gateways and openers settings and should be a bit more intuitive to use.

## 2.0.10

  * Improved state handling for opening and closing conditions, including dealing with edge cases.
  * Preserve door state information across homebridge instances, so we remember where we left off.
  * myQ API cleanup.

## 2.0.1 - 2.0.9 (2020-07-04)

  * API fixes to ensure compatibility.
  * Re-include UI-based configuration.
  * Re-include README and CHANGELOG.
  * Broaden our filtering for garage door openers (who knew there were so many types?!) :smile:

  Thanks to [shamoon](https://github.com/shamoon) and others for debugging and contributing to the API fixes and troubleshooting.


## 2.0.0 (2020-07-03)

  ### Breaking Changes

		* Plugin requires homebridge >= 1.0.0.
		* This plugin has been refactored to typescript.
		* Update to myQ API v5.1.
		* Configuration changes:
			* Platform name has changed to `myQ`. **This will break existing configurations, so ensure you regenerate or update your `config.json` accordingly**.
			* The settings `gateways` and `openers` still exist but currently do nothing. This will be fixed in a future release.
			* Battery status is no longer provided as it doesn't seem to exist in the most recent myQ API. **If you were using this feature, please open an issue and the author can work with you to determine if the API exposes this functionality and make it available in this plugin**.


================================================
FILE: docs/FeatureOptions.md
================================================
<SPAN ALIGN="CENTER" STYLE="text-align:center">
<DIV ALIGN="CENTER" STYLE="text-align:center">

[![homebridge-myq: Native HomeKit support for myQ garage door openers and other devices](https://raw.githubusercontent.com/hjdhjd/homebridge-myq/main/homebridge-myq.svg)](https://github.com/hjdhjd/homebridge-myq)

# Homebridge myQ

[![Downloads](https://img.shields.io/npm/dt/homebridge-myq?color=%235EB5E5&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/homebridge-myq)
[![Version](https://img.shields.io/npm/v/homebridge-myq?color=%235EB5E5&label=Homebridge%20myQ&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/homebridge-myq)
[![myQ@Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=%235EB5E5&label=Discord&logo=discord&logoColor=%23FFFFFF&style=for-the-badge)](https://discord.gg/QXqfHEW)
[![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%2357277C&style=for-the-badge&logoColor=%23FFFFFF&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5OTIuMDkiIGhlaWdodD0iMTAwMCIgdmlld0JveD0iMCAwIDk5Mi4wOSAxMDAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYSIgZD0iTTk1MC4xOSw1MDguMDZhNDEuOTEsNDEuOTEsMCwwLDEtNDItNDEuOWMwLS40OC4zLS45MS4zLTEuNDJMODI1Ljg2LDM4Mi4xYTc0LjI2LDc0LjI2LDAsMCwxLTIxLjUxLTUyVjEzOC4yMmExNi4xMywxNi4xMywwLDAsMC0xNi4wOS0xNkg3MzYuNGExNi4xLDE2LjEsMCwwLDAtMTYsMTZWMjc0Ljg4bC0yMjAuMDktMjEzYTE2LjA4LDE2LjA4LDAsMCwwLTIyLjY0LjE5TDYyLjM0LDQ3Ny4zNGExNiwxNiwwLDAsMCwwLDIyLjY1bDM5LjM5LDM5LjQ5YTE2LjE4LDE2LjE4LDAsMCwwLDIyLjY0LDBMNDQzLjUyLDIyNS4wOWE3My43Miw3My43MiwwLDAsMSwxMDMuNjIuNDVMODYwLDUzOC4zOGE3My42MSw3My42MSwwLDAsMSwwLDEwNGwtMzguNDYsMzguNDdhNzMuODcsNzMuODcsMCwwLDEtMTAzLjIyLjc1TDQ5OC43OSw0NjguMjhhMTYuMDUsMTYuMDUsMCwwLDAtMjIuNjUuMjJMMjY1LjMsNjgwLjI5YTE2LjEzLDE2LjEzLDAsMCwwLDAsMjIuNjZsMzguOTIsMzlhMTYuMDYsMTYuMDYsMCwwLDAsMjIuNjUsMGwxMTQtMTEyLjM5YTczLjc1LDczLjc1LDAsMCwxLDEwMy4yMiwwbDExMywxMTEsLjQyLjQyYTczLjU0LDczLjU0LDAsMCwxLDAsMTA0TDU0NS4wOCw5NTcuMzV2LjcxYTQxLjk1LDQxLjk1LDAsMSwxLTQyLTQxLjk0Yy41MywwLC45NS4zLDEuNDQuM0w2MTYuNDMsODA0LjIzYTE2LjA5LDE2LjA5LDAsMCwwLDQuNzEtMTEuMzMsMTUuODUsMTUuODUsMCwwLDAtNC43OS0xMS4zMmwtMTEzLTExMWExNi4xMywxNi4xMywwLDAsMC0yMi42NiwwTDM2Ny4xNiw3ODIuNzlhNzMuNjYsNzMuNjYsMCwwLDEtMTAzLjY3LS4yN2wtMzktMzlhNzMuNjYsNzMuNjYsMCwwLDEsMC0xMDMuODZMNDM1LjE3LDQyNy44OGE3My43OSw3My43OSwwLDAsMSwxMDMuMzctLjlMNzU4LjEsNjM5Ljc1YTE2LjEzLDE2LjEzLDAsMCwwLDIyLjY2LDBsMzguNDMtMzguNDNhMTYuMTMsMTYuMTMsMCwwLDAsMC0yMi42Nkw1MDYuNSwyNjUuOTNhMTYuMTEsMTYuMTEsMCwwLDAtMjIuNjYsMEwxNjQuNjksNTgwLjQ0QTczLjY5LDczLjY5LDAsMCwxLDYxLjEsNTgwTDIxLjU3LDU0MC42OWwtLjExLS4xMmE3My40Niw3My40NiwwLDAsMSwuMTEtMTAzLjg4TDQzNi44NSwyMS40MUE3My44OSw3My44OSwwLDAsMSw1NDAsMjAuNTZMNjYyLjYzLDEzOS4zMnYtMS4xYTczLjYxLDczLjYxLDAsMCwxLDczLjU0LTczLjVINzg4YTczLjYxLDczLjYxLDAsMCwxLDczLjUsNzMuNVYzMjkuODFhMTYsMTYsMCwwLDAsNC43MSwxMS4zMmw4My4wNyw4My4wNWguNzlhNDEuOTQsNDEuOTQsMCwwLDEsLjA4LDgzLjg4WiIvPjwvc3ZnPg==)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)

## myQ garage door and other myQ-enabled device support for [Homebridge](https://homebridge.io).
</DIV>
</SPAN>

`homebridge-myq` is a [Homebridge](https://homebridge.io) plugin that makes myQ-enabled devices available to [Apple's](https://www.apple.com) [HomeKit](https://www.apple.com/ios/home) smart home platform. myQ-enabled devices include many smart garage door openers made primarily by Liftmaster, Chamberlain, and Craftsman, but includes other brands as well. You can determine if your garage door or other device is myQ-enabled by checking the [myQ compatibility check tool](https://www.myq.com/myq-compatibility) on the myQ website.

### Feature Options

Feature options allow you to enable or disable certain features in this plugin. These feature options provide unique flexibility by also allowing you to set a scope for each option that allows you more granular control in how this plugin makes features and capabilities available in HomeKit.

The priority given to these options works in the following order, from highest to lowest priority where settings that are higher in priority will override the ones below:

  * Device options that are enabled or disabled.
  * Global options that are enabled or disabled.

All feature options can be set at any scope level, or at multiple scope levels. If an option isn't applicable to a particular category of device, it is ignored. If you want to override a global feature option you've set, you can override the global feature option for the individual device, if you choose.

**Note: it's strongly recommended that you use the Homebridge webUI](https://github.com/homebridge/homebridge-config-ui-x) to configure this plugin - it's easier to use for most people, and will ensure you always have a valid configuration.**

#### Specifying Scope
Scoping rules:

  * If you don't use a scoping specifier, feature options will be applied globally for all devices and streaming clients.
  * To use a device-specific feature option, append the option with `.serial`, where `serial` is the serial number of the myQ device, as shown in the `hombridge-myq` logs within Homebridge.

`homebridge-myq` will log all devices it discovers on startup, including serial numbers, which you can use to tailor the feature options you'd like to enable or disable on a per-device basis.

### Getting Started
Before using these features, you should understand how feature options propagate the devices attached to them. If you've disabled an option globally, you can selectively enable an option on a single device by explicitly using `Enable.` Feature Option with that device's serial number. This provides you a lot of richness in how you enable or disable devices for HomeKit use.

The `options` setting is an array of strings used to customize Feature Options in your `config.json`. I would encourage most users, however, to use the [Homebridge webUI](https://github.com/homebridge/homebridge-config-ui-x), to configure Feature Options as well as other options in this plugin. It contains additional validation checking of parameters to ensure the configuration is always valid.

#### Example Configuration
An example `options` setting might look like this in your config.json:

```js
"platforms": [
  {
    "platform": "myQ",

    "options": [
      "Disable.Device.CG12345",
      "Enable.Device.CG6789"
    ],

    "email": "email@email.com",
    "password": "password"
  }
]
```
In this example:

* The first line `Disable.Device.CG12345` prevents the door opener with the serial number `CG12345` from appearing in HomeKit.
* The second line `Enable.Device.CG6789` shows the door opener with the serial number `CG6789` in HomeKit. In this instance, an option such as this one is unnecessary given that myQ devices are shown by default in `homebridge-myq` and is provided as an example only.

### <A NAME="reference"></A>Feature Options Reference
Feature options provide a rich mechanism for tailoring your `homebridge-myq` experience. The reference below is divided into functional category groups:

**Note: it's strongly recommended that you use the Homebridge webUI](https://github.com/homebridge/homebridge-config-ui-x) to configure this plugin - it's easier to use for most people, and will ensure you always have a valid configuration.**

 * [Device](#device): Device feature options.
 * [Opener](#opener): Opener feature options.

#### <A NAME="device"></A>Device feature options.

These option(s) apply to: all myQ devices

| Option                                           | Description
|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| `Device`                                         | Make this device available in HomeKit. **(default: enabled)**.
| `Device.SyncNames`                               | Synchronize the myQ name of this device with HomeKit. Synchronization is one-way only, syncing the device name from myQ to HomeKit. **(default: disabled)**.

#### <A NAME="opener"></A>Opener feature options.

These option(s) apply to: myQ garage door and gate openers

| Option                                           | Description
|--------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| `Opener.ReadOnly`                                | Make this opener read-only by ignoring open and close requests from HomeKit. **(default: disabled)**.
| `Opener.BatteryInfo`                             | Display battery status information for myQ door position sensors. You may want to disable this if the myQ status information is incorrectly resulting in a potential notification annoyance in the Home app. **(default: enabled)**. <BR>*Supported on myQ devices that have a door position sensor.*
| `Opener.Switch`                                  | Add a switch accessory to control the opener. This can be useful in automation scenarios where you want to work around HomeKit's security restrictions for controlling garage door openers. **(default: disabled)**.
| `Opener.OccupancySensor`                         | Add an occupancy sensor accessory using the open state of the opener to determine occupancy. This can be useful in automation scenarios where you want to trigger an action based on the opener being open for an extended period of time. **(default: disabled)**.
| `Opener.OccupancySensor.Duration<I>.Value</I>`   | Duration, in seconds, to wait once the opener has reached the open state before indicating occupancy. **(default: 300)**.


================================================
FILE: docs/MQTT.md
================================================
<SPAN ALIGN="CENTER" STYLE="text-align:center">
<DIV ALIGN="CENTER" STYLE="text-align:center">

[![homebridge-myq: Native HomeKit support for myQ garage door openers and other devices](https://raw.githubusercontent.com/hjdhjd/homebridge-myq/main/homebridge-myq.svg)](https://github.com/hjdhjd/homebridge-myq)

# Homebridge myQ

[![Downloads](https://img.shields.io/npm/dt/homebridge-myq?color=%235EB5E5&logo=icloud&logoColor=%23FFFFFF&style=for-the-badge)](https://www.npmjs.com/package/homebridge-myq)
[![Version](https://img.shields.io/npm/v/homebridge-myq?color=%235EB5E5&label=Homebridge%20myQ&logoColor=%23FFFFFF&style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBzdHlsZT0iZmlsbDojRkZGRkZGIiBkPSJNMjMuOTkzIDkuODE2TDEyIDIuNDczbC00LjEyIDIuNTI0VjIuNDczSDQuMTI0djQuODE5TC4wMDQgOS44MTZsMS45NjEgMy4yMDIgMi4xNi0xLjMxNXY5LjgyNmgxNS43NDl2LTkuODI2bDIuMTU5IDEuMzE1IDEuOTYtMy4yMDIiLz48L3N2Zz4K)](https://www.npmjs.com/package/homebridge-myq)
[![myQ@Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=%235EB5E5&label=Discord&logo=discord&logoColor=%23FFFFFF&style=for-the-badge)](https://discord.gg/QXqfHEW)
[![verified-by-homebridge](https://img.shields.io/badge/homebridge-verified-blueviolet?color=%2357277C&style=for-the-badge&logoColor=%23FFFFFF&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5OTIuMDkiIGhlaWdodD0iMTAwMCIgdmlld0JveD0iMCAwIDk5Mi4wOSAxMDAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iYSIgZD0iTTk1MC4xOSw1MDguMDZhNDEuOTEsNDEuOTEsMCwwLDEtNDItNDEuOWMwLS40OC4zLS45MS4zLTEuNDJMODI1Ljg2LDM4Mi4xYTc0LjI2LDc0LjI2LDAsMCwxLTIxLjUxLTUyVjEzOC4yMmExNi4xMywxNi4xMywwLDAsMC0xNi4wOS0xNkg3MzYuNGExNi4xLDE2LjEsMCwwLDAtMTYsMTZWMjc0Ljg4bC0yMjAuMDktMjEzYTE2LjA4LDE2LjA4LDAsMCwwLTIyLjY0LjE5TDYyLjM0LDQ3Ny4zNGExNiwxNiwwLDAsMCwwLDIyLjY1bDM5LjM5LDM5LjQ5YTE2LjE4LDE2LjE4LDAsMCwwLDIyLjY0LDBMNDQzLjUyLDIyNS4wOWE3My43Miw3My43MiwwLDAsMSwxMDMuNjIuNDVMODYwLDUzOC4zOGE3My42MSw3My42MSwwLDAsMSwwLDEwNGwtMzguNDYsMzguNDdhNzMuODcsNzMuODcsMCwwLDEtMTAzLjIyLjc1TDQ5OC43OSw0NjguMjhhMTYuMDUsMTYuMDUsMCwwLDAtMjIuNjUuMjJMMjY1LjMsNjgwLjI5YTE2LjEzLDE2LjEzLDAsMCwwLDAsMjIuNjZsMzguOTIsMzlhMTYuMDYsMTYuMDYsMCwwLDAsMjIuNjUsMGwxMTQtMTEyLjM5YTczLjc1LDczLjc1LDAsMCwxLDEwMy4yMiwwbDExMywxMTEsLjQyLjQyYTczLjU0LDczLjU0LDAsMCwxLDAsMTA0TDU0NS4wOCw5NTcuMzV2LjcxYTQxLjk1LDQxLjk1LDAsMSwxLTQyLTQxLjk0Yy41MywwLC45NS4zLDEuNDQuM0w2MTYuNDMsODA0LjIzYTE2LjA5LDE2LjA5LDAsMCwwLDQuNzEtMTEuMzMsMTUuODUsMTUuODUsMCwwLDAtNC43OS0xMS4zMmwtMTEzLTExMWExNi4xMywxNi4xMywwLDAsMC0yMi42NiwwTDM2Ny4xNiw3ODIuNzlhNzMuNjYsNzMuNjYsMCwwLDEtMTAzLjY3LS4yN2wtMzktMzlhNzMuNjYsNzMuNjYsMCwwLDEsMC0xMDMuODZMNDM1LjE3LDQyNy44OGE3My43OSw3My43OSwwLDAsMSwxMDMuMzctLjlMNzU4LjEsNjM5Ljc1YTE2LjEzLDE2LjEzLDAsMCwwLDIyLjY2LDBsMzguNDMtMzguNDNhMTYuMTMsMTYuMTMsMCwwLDAsMC0yMi42Nkw1MDYuNSwyNjUuOTNhMTYuMTEsMTYuMTEsMCwwLDAtMjIuNjYsMEwxNjQuNjksNTgwLjQ0QTczLjY5LDczLjY5LDAsMCwxLDYxLjEsNTgwTDIxLjU3LDU0MC42OWwtLjExLS4xMmE3My40Niw3My40NiwwLDAsMSwuMTEtMTAzLjg4TDQzNi44NSwyMS40MUE3My44OSw3My44OSwwLDAsMSw1NDAsMjAuNTZMNjYyLjYzLDEzOS4zMnYtMS4xYTczLjYxLDczLjYxLDAsMCwxLDczLjU0LTczLjVINzg4YTczLjYxLDczLjYxLDAsMCwxLDczLjUsNzMuNVYzMjkuODFhMTYsMTYsMCwwLDAsNC43MSwxMS4zMmw4My4wNyw4My4wNWguNzlhNDEuOTQsNDEuOTQsMCwwLDEsLjA4LDgzLjg4WiIvPjwvc3ZnPg==)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)

## myQ garage door and other myQ-enabled device support for [Homebridge](https://homebridge.io).
</DIV>
</SPAN>

`homebridge-myq` is a [Homebridge](https://homebridge.io) plugin that makes myQ-enabled devices available to [Apple's](https://www.apple.com) [HomeKit](https://www.apple.com/ios/home) smart home platform. myQ-enabled devices include many smart garage door openers made primarily by Liftmaster, Chamberlain, and Craftsman, but includes other brands as well. You can determine if your garage door or other device is myQ-enabled by checking the [myQ compatibility check tool](https://www.myq.com/myq-compatibility) on the myQ website.

### MQTT Support
[MQTT](https://mqtt.org) is a popular Internet of Things (IoT) messaging protocol that can be used to weave together different smart devices and orchestrate or instrument them in an infinite number of ways. In short - it lets things that might not normally be able to talk to each other communicate across ecosystems, provided they can support MQTT.

I've provided MQTT support for those that are interested - I'm genuinely curious, if not a bit skeptical, at how many people actually want to use this capability. MQTT has a lot of nerd-credibility, and it was a fun side project to mess around with. :smile:

`homebridge-myq` will publish MQTT events if you've configured a broker in the controller-specific settings. The plugin supports a rich set of capabilities over MQTT. This includes:

  * Garage open and close events.

### How to configure and use this feature

This documentation assumes you know what MQTT is, what an MQTT broker does, and how to configure it. Setting up an MQTT broker is beyond the scope of this documentation. There are plenty of guides available on how to do so just a search away.

You configure MQTT settings within a `controller` configuration block. The settings are:

| Configuration Setting | Description
|-----------------------|----------------------------------
| **mqttUrl**           | The URL of your MQTT broker. **This must be in URL form**, e.g.: `mqtt://user:password@1.2.3.4`.
| **mqttTopic**         | The base topic to publish to. The default is: `myq`.

To reemphasize the above: **mqttUrl** must be a valid URL. Just entering a hostname will result in an error. The URL can use any of these protocols: `mqtt`, `mqtts`, `tcp`, `tls`, `ws`, `wss`.

When events are published, by default, the topics look like:

```sh
myq/1234567890AB/garagedoor
```

In the above example, `1234567890AB` is the serial number of your garage door opener. We use serial numbers as an easy way to guarantee unique identifiers that won't change. `homebridge-myq` provides you information about your myQ devices and their respective serial numbers in the Homebridge log on startup.

### <A NAME="publish"></A>Topics Published
The topics and messages that `homebridge-myq` publishes are:

| Topic                 | Message Published
|-----------------------|----------------------------------
| **garagedoor**        | `closed`, `closing`, `open`, `opening`, when garage door state changes are detected.

Messages are published to MQTT when an action occurs on a device that triggers the respective event, or when an MQTT message is received for one of the topics `homebridge-myq` subscribes to.

### <A NAME="subscribe"></A>Topics Subscribed
The topics that `homebridge-myq` subscribes to are:

| Topic                   | Message Expected
|-------------------------|----------------------------------
| **garagedoor/get**      | `true` will request that the plugin publish the current state of the garage door to the `garagedoor` topic.
| **garagedoor/set**      | One of `close` or `open`. This will send the respective command to the garage door.

### Some Fun Facts
  * MQTT support is disabled by default. It's enabled when an MQTT broker is specified in the configuration.
  * If connectivity to the broker is lost, it will perpetually retry to connect in one-minute intervals.
  * If a bad URL is provided, MQTT support will not be enabled.


================================================
FILE: homebridge-ui/public/index.html
================================================
<p class="text-center">
  <img src="https://raw.githubusercontent.com/hjdhjd/homebridge-myq/main/homebridge-myq.svg" alt="homebridge-myq logo" class="w-50" />
</p>
<div id="pageFirstRun" style="display: none;">
  <div class="text-center">
    <p>Please enter your myQ login credentials to get started with <strong>homebridge-myq</strong>.</p>
    <table class="table table-sm table-borderless">
      <tr>
        <td>
          <input type="email" placeholder="myQ email address" size="30" id="email"></input>
        </td>
      </tr>
      <tr>
        <td>
          <input type="password" placeholder="myQ password" size="30" id="password"></input>
        </td>
      </tr>
      <tr>
        <td class="text-danger" id="loginError">
          &nbsp;
        </td>
      </tr>
    </table>
    <br>
      <button type="button" class="btn btn-primary" id="firstRun">Login to myQ &rarr;</button>
    <br>
      To optimize performance and responsiveness, please make this plugin a <a target="_blank" href="https://github.com/homebridge/homebridge/wiki/Child-Bridges">child bridge</a> once you've completed configuration.
  </div>
</div>
<div id="menuWrapper" class="btn-group w-100 mb-0" role="group" aria-label="UI Menu" style="display: none;">
  <button type="button" class="btn btn-primary" id="menuSettings">Settings</button>
  <button type="button" class="btn btn-primary" id="menuFeatureOptions">Feature Options</button>
  <button type="button" class="btn btn-primary mr-0" id="menuHome">Support</button>
</div>
<div id="pageFeatureOptions" class="mt-4" style="display: none;">
  <div id="deviceInfo">
    <table class="table table-sm table-borderless">
      <tr class="align-center">
        <td id="headerInfo" colspan="2" class="m-0 p-2 text-center font-weight-bold"></td>
      </tr>
      <tr class="align-top">
        <td rowspan="3" class="w-25">
          <table id="sidebar" class="table table-sm table-bordered m-0 p-0">
            <tr>
              <td>
                <table class="table table-sm table-borderless m-0 p-0" id="controllersTable"></table>
              </td>
            </tr>
            <tr>
              <td>
                <table class="table table-sm table-borderless m-0 p-0" id="devicesTable"></table>
              </td>
            </tr>
          </table>
        </td>
        <td id="deviceStatsTable">
          <table class="table table-sm table-borderless border-bottom m-0 p-0">
            <tr id="deviceStatsHeader">
              <th class="m-0 p-0" style="width: 60%;"><B>Model<B></th>
              <th class="m-0 p-0 w-25"><B>Serial</B></th>
              <th class="m-0 p-0" style="width: 15%;"><B>Status</B></th>
            </tr>
            <tr>
              <td id="device_model" class="m-0 p-0"></td>
              <td id="device_serial" class="m-0 p-0"></td>
              <td id="device_online" class="m-0 p-0"></td>
            </tr>
          </table>
        </td>
      </tr>
      <tr>
        <td id="configTable" class="w-100"></td>
      </tr>
    </table>
  </div>
</div>
<div id="pageSupport" class="mt-4" style="display: none;">
  <h5>Introduction</h5>
    <p class="px-4">I hope you enjoy <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq">homebridge-myq</a> as much as I enjoy developing it. All my projects are labors of love. If you'd like to show your appreciation - <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq">star this project on Github</A> and do some good in your community, either financially or with your time: a food bank, an animal shelter (two of my passions), or whatever resonates with you that can give something back to the world around you.</p>

    <div class="px-4">
      Other plugins by <a target="_blank" href="https://github.com/hjdhjd">HJD</a>:

      <ul dir="auto">
        <li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect">homebridge-unifi-protect: Complete HomeKit integration for the entire UniFi Protect ecosystem</a></li>
      </ul>
    </div>

  <h5>Getting Started</h5>
    <ul dir="auto">
      <li>
        <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq#installation">Installation</a>: installing this plugin, including system requirements.
      </li>
      <li>
        <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq#plugin-configuration">Plugin Configuration</a>: how to quickly get up and running.
      </li>
      <li>
        <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq#notes">Additional Notes</a>: some things you should be aware of, including myQ-specific quirks.
      </li>
    </ul>

  <h5>Advanced Topics</h5>
    <ul dir="auto">
      <li>
        <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq/blob/main/docs/FeatureOptions.md">Feature Options</a>: granular options to allow you to show or hide specific garage door and gate openers, and more.
      </li>
      <li>
        <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq/blob/main/docs/MQTT.md">MQTT</a>: how to configure MQTT support.
      </li>
      <li>
        <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq/blob/main/docs/AdvancedOptions.md">Advanced Configuration</a>: complete list of configuration options available in this plugin.
      </li>
    </ul>

  <h5>Support</h5>
  <div class="px-4">The myQ API has largely stabilized in recent years, particularly with the advent of the v6 version of the API. However, the myQ cloud can (and does) have occasional reliability challenges that can manifest through connectivity issues that you will see in the Homebridge logs related to this plugin. These are not bugs in homebridge-myq and there's not much we can do it aside from attempt to gracefully handle the errors when they occur. The myQ cloud is quite reliable the large majority (~93-95% by my estimate) of the time.<br><br><strong class="text-danger">Please do not open bug reports related to API issues - they'll be closed summarily unless it's a new API-specific issue that's emerged as a result of changes to the undocumented myQ API.</strong></div>
  <br>
  <ul>
    <li>
      <a target="_blank" href="https://discord.gg/QXqfHEW">Discord Support Channel</a>
    </li>
    <li>
      <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq/issues/new/choose">Create a Developer Support Request</a>
    </li>
    <li>
      <a target="_blank" href="https://github.com/hjdhjd/homebridge-myq/blob/main/docs/Changelog.md">Changelog and Release Notes</a>
    </li>
  </ul>
</div>
<script src="./ui.mjs" type="module"></script>


================================================
FILE: homebridge-ui/public/lib/featureoptions.mjs
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * featureoptions.mjs: Feature option webUI base class.
 */
"use strict";

export class FeatureOptions {

  controller;

  featureOptionGroups;
  featureOptionList;
  optionsList;

  constructor() {

    this.featureOptionGroups = {};
    this.featureOptionList = {};
    this.controller = null;
    this.optionsList = [];
  }

  // Abstract method to be implemented by subclasses to render the feature option webUI.
  async showUI() {
  }

  // Is this feature option set explicitly?
  isOptionSet(featureOption, deviceMac) {

    const optionRegex = new RegExp("^(?:Enable|Disable)\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "$", "gi");
    return this.optionsList.filter(x => optionRegex.test(x)).length ? true : false;
  }

  // Is a feature option globally enabled?
  isGlobalOptionEnabled(featureOption, defaultState) {

    featureOption = featureOption.toUpperCase();

    // Test device-specific options.
    return this.optionsList.some(x => x === ("ENABLE." + featureOption)) ? true :
      (this.optionsList.some(x => x === ("DISABLE." + featureOption)) ? false : defaultState
      );
  }

  // Is a feature option enabled at the device or global level. This function does not traverse the scoping hierarchy.
  isDeviceOptionEnabled(featureOption, mac, defaultState) {

    if(!mac) {

      return this.isGlobalOptionEnabled(featureOption, defaultState);
    }

    featureOption = featureOption.toUpperCase();
    mac = mac.toUpperCase();

    // Test device-specific options.
    return this.optionsList.some(x => x === ("ENABLE." + featureOption + "." + mac)) ? true :
      (this.optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false : defaultState
      );
  }

  // Is a value-centric feature option enabled at the device or global level. This function does not traverse the scoping hierarchy.
  isOptionValueSet(featureOption, deviceMac) {

    const optionRegex = new RegExp("^Enable\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "\\.([^\\.]+)$", "gi");

    return this.optionsList.filter(x => optionRegex.test(x)).length ? true : false;
  }

  // Get the value of a value-centric feature option.
  getOptionValue(featureOption, deviceMac) {

    const optionRegex = new RegExp("^Enable\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "\\.([^\\.]+)$", "gi");

    // Get the option value, if we have one.
    for(const option of this.optionsList) {

      const regexMatch = optionRegex.exec(option);

      if(regexMatch) {

        return regexMatch[1];
      }
    }

    return undefined;
  }

  // Is a feature option enabled at the device or global level. It does traverse the scoping hierarchy.
  isOptionEnabled(featureOption, deviceMac) {

    const defaultState = this.featureOptionList[featureOption]?.default ?? true;

    if(deviceMac) {

      // Device level check.
      if(this.isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) {

        return !defaultState;
      }

      // Controller level check.
      if(this.isDeviceOptionEnabled(featureOption, this.controller, defaultState) !== defaultState) {

        return !defaultState;
      }
    }

    // Global check.
    if(this.isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) {

      return !defaultState;
    }

    // Return the default.
    return defaultState;
  };

  // Return the scope level of a feature option.
  optionScope(featureOption, deviceMac, defaultState, isOptionValue = false) {

    // Scope priority is always: device, controller, global.

    // If we have a value-centric feature option, our lookups are a bit different.
    if(isOptionValue) {

      if(deviceMac) {

        if(this.isOptionValueSet(featureOption, deviceMac)) {

          return "device";
        }

        if(this.isOptionValueSet(featureOption, this.controller)) {

          return "controller";
        }
      }

      if(this.isOptionValueSet(featureOption)) {

        return "global";
      }

      return "none";
    }

    if(deviceMac) {

      // Let's see if we've set it at the device-level.
      if((this.isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) || this.isOptionSet(featureOption, deviceMac)) {

        return "device";
      }

      // Now let's test the controller level.
      if((this.isDeviceOptionEnabled(featureOption, this.controller, defaultState) !== defaultState) || this.isOptionSet(featureOption, this.controller)) {

        return "controller";
      }
    }

    // Finally, let's test the global level.
    if((this.isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) || this.isOptionSet(featureOption)) {

      return "global";
    }

    // Option isn't set to a non-default value.
    return "none";
  };

  // Return the color hinting for a given option's scope.
  optionScopeColor(featureOption, deviceMac, defaultState, isOptionValue) {

    switch(this.optionScope(featureOption, deviceMac, defaultState, isOptionValue)) {

      case "device":

        return "text-info";
        break;

      case "controller":

        return "text-success";
        break;

      case "global":

        return deviceMac ? "text-warning" : "text-info";
        break;

      default:

        break;
    }

    return null;
  };
}


================================================
FILE: homebridge-ui/public/myq-featureoptions.mjs
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-featureoptions.mjs: myQ feature option webUI.
 */
"use strict";

import { FeatureOptions} from "./lib/featureoptions.mjs";

export class myQFeatureOptions extends FeatureOptions {

  // The current plugin configuration.
  currentConfig;

  // Current configuration options selected in the webUI for a given device.
  #configOptions;

  // Table containing the currently displayed feature options.
  #configTable;

  // Current list of devices on a given controller, for webUI elements.
  #deviceList;

  // Current list of myQ devices from the myQ API.
  #myQDevices;

  constructor() {

    super();

    this.configOptions = [];
    this.configTable = document.getElementById("configTable");
    this.currentConfig = [];
    this.deviceList = [];
    this.myQDevices = [];
  }

  // Render the feature option webUI.
  async showUI() {

    // Show the beachball while we setup.
    homebridge.showSpinner();
    homebridge.hideSchemaForm();

    // Make sure we have the refreshed configuration.
    this.currentConfig = await homebridge.getPluginConfig();

    // Create our custom UI.
    document.getElementById("menuHome").classList.remove("btn-elegant");
    document.getElementById("menuHome").classList.add("btn-primary");
    document.getElementById("menuFeatureOptions").classList.add("btn-elegant");
    document.getElementById("menuFeatureOptions").classList.remove("btn-primary");
    document.getElementById("menuSettings").classList.remove("btn-elegant");
    document.getElementById("menuSettings").classList.add("btn-primary");

    // Hide the legacy UI.
    document.getElementById("pageSupport").style.display = "none";
    document.getElementById("pageFeatureOptions").style.display = "block";

    // What we're going to do is display our global options, followed by the list of devices provided by the myQ API.
    // We pre-select our global options by default for the user as a starting point.

    // Retrieve the table for the our list of controllers and global options.
    const controllersTable = document.getElementById("controllersTable");

    // Start with a clean slate.
    controllersTable.innerHTML = "";
    document.getElementById("devicesTable").innerHTML = "";
    this.configTable.innerHTML = "";
    this.deviceList = [];

    // Hide the UI until we're ready.
    document.getElementById("sidebar").style.display = "none";
    document.getElementById("headerInfo").style.display = "none";
    document.getElementById("deviceStatsTable").style.display = "none";

    // We haven't configured anything yet - we're done.
    if(!this.currentConfig[0]?.email?.length || !this.currentConfig[0]?.password?.length) {

      document.getElementById("headerInfo").innerHTML = "Please configure your myQ login credentials in the settings tab before configuring feature options.";
      document.getElementById("headerInfo").style.display = "";
      homebridge.hideSpinner();

      return;
    }

    // Initialize our informational header.
    document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:<br><i class=\"text-warning\">Global options</i> (lowest priority) &rarr; <i class=\"text-info\">myQ device options</i> (highest priority)";

    // Enumerate our global options.
    const trGlobal = document.createElement("tr");

    // Create the cell for our global options.
    const tdGlobal = document.createElement("td");
    tdGlobal.classList.add("m-0", "p-0");

    // Create our label target.
    const globalLabel = document.createElement("label");

    globalLabel.name = "Global Options";
    globalLabel.appendChild(document.createTextNode("Global Options"));
    globalLabel.style.cursor = "pointer";
    globalLabel.classList.add("mx-2", "my-0", "p-0", "w-100");

    globalLabel.addEventListener("click", event => this.#showDevices(true));

    // Add the global options label.
    tdGlobal.appendChild(globalLabel);
    tdGlobal.style.fontWeight = "bold";

    // Add the global cell to the table.
    trGlobal.appendChild(tdGlobal);

    // Now add it to the overall controllers table.
    controllersTable.appendChild(trGlobal);

    // Add it as another device, for UI purposes.
    this.deviceList.push(globalLabel);

    // All done. Let the user interact with us.
    homebridge.hideSpinner();

    // Default the user on our global settings.
    this.#showDevices(true);
  }

  // Show the device list.
  async #showDevices(isGlobal) {

    // Show the beachball while we setup.
    homebridge.showSpinner();

    const devicesTable = document.getElementById("devicesTable");
    this.myQDevices = [];

    // If we're not accessing global options, pull a list of devices attached to this controller.
    this.myQDevices = await homebridge.request("/getDevices", { email: this.currentConfig[0].email, password: this.currentConfig[0].password, myQRegion: this.currentConfig[0].myQRegion });

    // Couldn't connect to the myQ API for some reason.
    if((this.myQDevices?.length === 1) && this.myQDevices[0] === -1) {

      devicesTable.innerHTML = "";
      this.configTable.innerHTML = "";
      document.getElementById("deviceStatsTable").style.display = "none";
      document.getElementById("sidebar").style.display = "none";

      document.getElementById("headerInfo").innerHTML = "Unable to connect to the myQ API.<br>Check your username and password in the settings tab to verify they are correct, or try again later if the myQ API is currently having difficulties.<br><code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>";
      document.getElementById("headerInfo").style.display = "";

      homebridge.hideSpinner();
      return;
    }

    // Make the UI visible.
    document.getElementById("sidebar").style.display = "";
    document.getElementById("headerInfo").style.display = "";

    const familyKeys = [...new Set(this.myQDevices.map(x => x.device_family))];

    // Wipe out the device list, except for our global entry.
    this.deviceList.splice(1, this.deviceList.length);

    // We aren't using the concept of a controller category in myQ, so we set this to something innocuous.
    this.controller = "NA";

    // Start with a clean slate.
    devicesTable.innerHTML = "";

    for(const key of familyKeys) {

      // Get all the devices associated with this device category.
      const devices = this.myQDevices.filter(x => x.device_family === key);

      // Create a row for this device category.
      const trCategory = document.createElement("tr");

      // Create the cell for our device category row.
      const tdCategory = document.createElement("td");
      tdCategory.classList.add("m-0", "p-0");

      // Add the category name, with appropriate casing.
      tdCategory.appendChild(document.createTextNode((key.charAt(0).toUpperCase() + key.slice(1) + "s")));
      tdCategory.style.fontWeight = "bold";

      // Add the cell to the table row.
      trCategory.appendChild(tdCategory);

      // Add the table row to the table.
      devicesTable.appendChild(trCategory);

      for(const device of devices) {

        // Create a row for this device.
        const trDevice = document.createElement("tr");
        trDevice.classList.add("m-0", "p-0");

        // Create a cell for our device.
        const tdDevice = document.createElement("td");
        tdDevice.classList.add("m-0", "p-0", "w-100");

        const label = document.createElement("label");

        label.name = device.serial_number;
        label.appendChild(document.createTextNode(device.name ?? device.device_family));
        label.style.cursor = "pointer";
        label.classList.add("mx-2", "my-0", "p-0", "w-100");

        label.addEventListener("click", event => this.#showDeviceInfo(device.serial_number));

        // Add the device label to our cell.
        tdDevice.appendChild(label);

        // Add the cell to the table row.
        trDevice.appendChild(tdDevice);

        // Add the table row to the table.
        devicesTable.appendChild(trDevice);

        this.deviceList.push(label);
      }
    }

    this.configOptions = [];

    // Initialize our feature option configuration.
    this.#updateConfigOptions(this.currentConfig[0].options ?? []);

    // Display the feature options to the user.
    this.#showDeviceInfo(isGlobal ? "Global Options" : this.myQDevices[0]?.serial_number);

    // All done. Let the user interact with us.
    homebridge.hideSpinner();
  }

  // Show feature option information for a specific device, controller, or globally.
  async #showDeviceInfo(deviceId) {

    homebridge.showSpinner();

    // Update the selected device for visibility.
    this.deviceList.map(x => (x.name === deviceId) ? x.parentElement.classList.add("bg-info", "text-white") : x.parentElement.classList.remove("bg-info", "text-white"));

    // Populate the device information info pane.
    const myQDevice = this.myQDevices.find(x => x.serial_number === deviceId);

    // Ensure we have a controller or device. The only time this won't be the case is when we're looking at global options.
    if(myQDevice) {

      document.getElementById("device_model").classList.remove("text-center");
      document.getElementById("device_model").colSpan = 1;
      document.getElementById("device_model").style.fontWeight = "normal";
      document.getElementById("device_model").innerHTML = myQDevice.hwInfo ? myQDevice.hwInfo.brand + " " + myQDevice.hwInfo.product : "Unknown";
      document.getElementById("device_serial").innerHTML = myQDevice.serial_number;
      document.getElementById("device_online").innerHTML = myQDevice.state.online ? "Online" : "Offline";
      document.getElementById("deviceStatsTable").style.display = "";
    } else {

      document.getElementById("deviceStatsTable").style.display = "none";
      document.getElementById("device_model").classList.remove("text-center");
      document.getElementById("device_model").colSpan = 1;
      document.getElementById("device_model").style.fontWeight = "normal";
      document.getElementById("device_model").innerHTML = "N/A"
      document.getElementById("device_serial").innerHTML = "N/A";
      document.getElementById("device_online").innerHTML = "N/A";
    }

    // Populate the feature options selected for this device.
    const myQFeatures = await homebridge.request("/getOptions", { configOptions: this.configOptions, myQDevice: myQDevice });
    const optionsDevice = myQFeatures.options;

    // Start with a clean slate.
    let newConfigTableHtml = "";
    this.configTable.innerHTML = "";

    // Initialize the full list of options.
    this.featureOptionList = {};
    this.featureOptionGroups = {};

    for(const category of myQFeatures.categories) {

      // Now enumerate all the feature options for a given device and add then to the full list.
      for(const option of optionsDevice[category.name]) {

        const featureOption = category.name + (option.name.length ? ("." + option.name): "");

        // Add it to our full list.
        this.featureOptionList[featureOption] = option;

        // Cross reference the feature option group it belongs to, if any.
        if(option.group !== undefined) {

          const expandedGroup = category.name + (option.group.length ? ("." + option.group): "");

          // Initialize the group entry if needed.
          if(!this.featureOptionGroups[expandedGroup]) {

            this.featureOptionGroups[expandedGroup] = [];
          }

          this.featureOptionGroups[expandedGroup].push(featureOption);
        }
      }
    }

    for(const category of myQFeatures.categories) {

      // Only show feature option categories that are valid for this context.
      if(myQDevice && !category.validFor.some(x => (x === myQDevice.device_family) || x === "all")) {

        continue;
      }

      const optionTable = document.createElement("table");
      const thead = document.createElement("thead");
      const tbody = document.createElement("tbody");
      const trFirst = document.createElement("tr");
      const th = document.createElement("th");

      // Set our table options.
      optionTable.classList.add("table", "table-borderless", "table-sm", "table-hover");
      th.classList.add("p-0");
      th.style.fontWeight = "bold";
      th.colSpan = 3;
      tbody.classList.add("table-bordered");

      // Add the feature option category description.
      th.appendChild(document.createTextNode(category.description + (!myQDevice ? " (Global)" : " (Device-specific)")));

      // Add the table header to the row.
      trFirst.appendChild(th);

      // Add the table row to the table head.
      thead.appendChild(trFirst);

      // Finally, add the table head to the table.
      optionTable.appendChild(thead);

      // Keep track of the number of options we have made available in a given category.
      let optionsVisibleCount = 0;

      // Now enumerate all the feature options for a given device.
      for(const option of optionsDevice[category.name]) {

        // Only show feature options that are valid for this device.
        if(myQDevice && option.hasProperty && !option.hasProperty.some(x => x in myQDevice)) {

          continue;
        }

        // Expand the full feature option.
        const featureOption = category.name + (option.name.length ? ("." + option.name): "");

        // Create the next table row.
        const trX = document.createElement("tr");
        trX.classList.add("align-top");
        trX.id = "row-" + featureOption;

        // Create a checkbox for the option.
        const tdCheckbox = document.createElement("td");

        // Create the actual checkbox for the option.
        const checkbox = document.createElement("input");

        checkbox.type = "checkbox";
        checkbox.readOnly = false;
        checkbox.id = featureOption;
        checkbox.name = featureOption;
        checkbox.value = featureOption + (!myQDevice ? "" : ("." + myQDevice.serial_number));

        let initialValue = undefined;
        let initialScope;

        // Determine our initial option scope to show the user what's been set.
        switch(initialScope = this.optionScope(featureOption, myQDevice?.serial_number, option.default, ("defaultValue" in option))) {

          case "global":
          case "controller":

            // If we're looking at the global scope, show the option value. Otherwise, we show that we're inheriting a value from the scope above.
            if(!myQDevice) {

              if("defaultValue" in option) {

                checkbox.checked = this.isOptionValueSet(featureOption);
                initialValue = this.getOptionValue(checkbox.id);
              } else {

                checkbox.checked = this.isGlobalOptionEnabled(featureOption, option.default);
              }

              if(checkbox.checked) {

                checkbox.indeterminate = false;
              }

            } else {

              if("defaultValue" in option) {

                initialValue = this.getOptionValue(checkbox.id, (initialScope === "controller") ? this.controller : undefined);
              }

              checkbox.readOnly = checkbox.indeterminate = true;
            }

            break;

          case "device":
          case "none":
          default:

            if("defaultValue" in option) {

              checkbox.checked = this.isOptionValueSet(featureOption, myQDevice?.serial_number);
              initialValue = this.getOptionValue(checkbox.id, myQDevice?.serial_number);
            } else {

              checkbox.checked = this.isDeviceOptionEnabled(featureOption, myQDevice?.serial_number, option.default);
            }

            break;
        }

        checkbox.defaultChecked = option.default;
        checkbox.classList.add("mx-2");

        // Add the checkbox to the table cell.
        tdCheckbox.appendChild(checkbox);

        // Add the checkbox to the table row.
        trX.appendChild(tdCheckbox);

        const tdLabel = document.createElement("td");
        tdLabel.classList.add("w-100");
        tdLabel.colSpan = 2;

        let inputValue = null;

        // Add an input field if we have a value-centric feature option.
        if(("defaultValue" in option)) {

          const tdInput = document.createElement("td");
          tdInput.classList.add("mr-2");
          tdInput.style.width = "10%";

          inputValue = document.createElement("input");
          inputValue.type = "text";
          inputValue.value = initialValue ?? option.defaultValue;
          inputValue.size = 5;
          inputValue.readOnly = !checkbox.checked;

          // Add or remove the setting from our configuration when we've changed our state.
          inputValue.addEventListener("change", async () => {

            // Find the option in our list and delete it if it exists.
            const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!myQDevice ? "" : ("\\." + myQDevice.serial_number)) + "\\.[^\\.]+$", "gi");
            const newOptions = this.configOptions.filter(x => !optionRegex.test(x));

            if(checkbox.checked) {

              newOptions.push("Enable." + checkbox.value + "." + inputValue.value);
            } else if(checkbox.indeterminate) {

              // If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
              inputValue.value = (myQDevice?.serial_number !== this.controller) ? (this.getOptionValue(checkbox.id, this.controller) ?? this.getOptionValue(checkbox.id)) : (this.getOptionValue(checkbox.id) ?? option.defaultValue);
            } else {

              inputValue.value = option.defaultValue;
            }

            // Update our configuration in Homebridge.
            this.currentConfig[0].options = newOptions;
            this.#updateConfigOptions(newOptions);
            await homebridge.updatePluginConfig(this.currentConfig);
          });

          tdInput.appendChild(inputValue);
          trX.appendChild(tdInput);
        }

        // Create a label for the checkbox with our option description.
        const labelDescription = document.createElement("label");
        labelDescription.for = checkbox.id;
        labelDescription.style.cursor = "pointer";
        labelDescription.classList.add("user-select-none", "my-0", "py-0");

        // Highlight options for the user that are different than our defaults.
        const scopeColor = this.optionScopeColor(featureOption, myQDevice?.serial_number, option.default, ("defaultValue" in option));

        if(scopeColor) {

          labelDescription.classList.add(scopeColor);
        }

        // Add or remove the setting from our configuration when we've changed our state.
        checkbox.addEventListener("change", async () => {

          // Find the option in our list and delete it if it exists.
          const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!myQDevice ? "" : ("\\." + myQDevice.serial_number)) + "$", "gi");
          const newOptions = this.configOptions.filter(x => !optionRegex.test(x));

          // Figure out if we've got the option set upstream.
          let upstreamOption = false;

          // We explicitly want to check for the scope of the feature option above where we are now, so we can appropriately determine what we should show.
          switch(this.optionScope(checkbox.id, (myQDevice && (myQDevice.serial_number !== this.controller)) ? this.controller : null, option.default, ("defaultValue" in option))) {

            case "device":
            case "controller":

              if(myQDevice.serial_number !== this.controller) {

                upstreamOption = true;
              }

              break;

            case "global":

              if(myQDevice) {

                upstreamOption = true;
              }

              break;

            default:

              break;
          }

          // For value-centric feature options, if there's an upstream value assigned above us, we don't allow for an unchecked state as it makes no sense in that context.
          if(checkbox.readOnly && (!("defaultValue" in option) || (("defaultValue" in option) && inputValue && !upstreamOption))) {

            // We're truly unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously,
            // so we use the readOnly property to let us know that we've just cycled from an indeterminate state.
            checkbox.checked = checkbox.readOnly = false;
          } else if(!checkbox.checked) {

            // If we have an upstream option configured, we reveal a third state to show inheritance of that option and allow the user to select it.
            if(upstreamOption) {

              // We want to set the readOnly property as well, since it will survive a user interaction when they click the checkbox to clear out the
              // indeterminate state. This allows us to effectively cycle between three states.
              checkbox.readOnly = checkbox.indeterminate = true;
            }

            if(("defaultValue" in option) && inputValue) {

              inputValue.readOnly = true;
            }
          } else if(checkbox.checked) {

            // We've explicitly checked this option.
            checkbox.readOnly = checkbox.indeterminate = false;

            if(("defaultValue" in option) && inputValue) {

              inputValue.readOnly = false;
            }
          }

          // The setting is different from the default, highlight it for the user, accounting for upstream scope, and add it to our configuration.
          if(!checkbox.indeterminate && ((checkbox.checked !== option.default) || upstreamOption)) {

            labelDescription.classList.add("text-info");
            newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value);
          } else {

            // We've reset to the defaults, remove our highlighting.
            labelDescription.classList.remove("text-info");
          }

          // Update our Homebridge configuration.
          if(("defaultValue" in option) && inputValue) {

            // Inform our value-centric feature option to update Homebridge.
            const changeEvent = new Event("change");

            inputValue.dispatchEvent(changeEvent);
          } else {

            // Update our configuration in Homebridge.
            this.currentConfig[0].options = newOptions;
            this.#updateConfigOptions(newOptions);
            await homebridge.updatePluginConfig(this.currentConfig);
          }

          // If we've reset to defaults, make sure our color coding for scope is reflected.
          if((checkbox.checked === option.default) || checkbox.indeterminate) {

            const scopeColor = this.optionScopeColor(featureOption, myQDevice?.serial_number, option.default, ("defaultValue" in option));

            if(scopeColor) {

              labelDescription.classList.add(scopeColor);
            }
          }

          // Adjust visibility of other feature options that depend on us.
          if(this.featureOptionGroups[checkbox.id]) {

            const entryVisibility = this.isOptionEnabled(featureOption, myQDevice?.serial_number) ? "" : "none";

            // Lookup each feature option setting and set the visibility accordingly.
            for(const entry of this.featureOptionGroups[checkbox.id]) {

              document.getElementById("row-" + entry).style.display = entryVisibility;
            }
          }
        });

        // Add the actual description for the option after the checkbox.
        labelDescription.appendChild(document.createTextNode(option.description));

        // Add the label to the table cell.
        tdLabel.appendChild(labelDescription);

        // Provide a cell-wide target to click on options.
        tdLabel.addEventListener("click", () => checkbox.click());

        // Add the label table cell to the table row.
        trX.appendChild(tdLabel);

        // Adjust the visibility of the feature option, if it's logically grouped.
        if((option.group !== undefined) && !this.isOptionEnabled(category.name + (option.group.length ? ("." + option.group): ""), myQDevice?.serial_number)) {

          trX.style.display = "none";
        } else {

          // Increment the visible option count.
          optionsVisibleCount++;
        }

        // Add the table row to the table body.
        tbody.appendChild(trX);
      }

      // Add the table body to the table.
      optionTable.appendChild(tbody);

      // If we have no options visible in a given category, then hide the entire category.
      if(!optionsVisibleCount) {

        optionTable.style.display = "none";
      }

      // Add the table to the page.
      this.configTable.appendChild(optionTable);
    }

    homebridge.hideSpinner();
  }

  // Update our configuration options.
  #updateConfigOptions(newConfig) {

    // Update our configuration.
    this.configOptions = newConfig;

    // Show all the valid options configured by the user.
    this.optionsList = this.configOptions.filter(x => x.match(/^(Enable|Disable)\.*/gi)).map(x => x.toUpperCase());
  }
}


================================================
FILE: homebridge-ui/public/ui.mjs
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * ui.mjs: myQ webUI.
 */
"use strict";

import { myQFeatureOptions } from "./myq-featureoptions.mjs";

// Keep a list of all the feature options and option groups.
const featureOptions = new myQFeatureOptions();

// Show the first run user experience if we don't have valid login credentials.
async function showFirstRun () {

  const buttonFirstRun = document.getElementById("firstRun");
  const inputEmail = document.getElementById("email");
  const inputPassword = document.getElementById("password");
  const tdLoginError = document.getElementById("loginError");

  // Pre-populate with anything we might already have in our configuration.
  inputEmail.value = featureOptions.currentConfig[0].email ?? "";
  inputPassword.value = featureOptions.currentConfig[0].password ?? "";

  // Clear login error messages when the login credentials change.
  inputEmail.addEventListener("input", () => {

    tdLoginError.innerHTML = "&nbsp;";
  });

  inputPassword.addEventListener("input", () => {

    tdLoginError.innerHTML = "&nbsp;";
  });

  // First run user experience.
  buttonFirstRun.addEventListener("click", async () => {

    // Show the beachball while we setup.
    homebridge.showSpinner();

    const email = inputEmail.value;
    const password = inputPassword.value;

    tdLoginError.innerHTML = "&nbsp;";

    if(!email?.length || !password?.length) {

      tdLoginError.appendChild(document.createTextNode("You haven't entered a valid email address and password."));
      homebridge.hideSpinner();
      return;
    }

    const myQDevices = await homebridge.request("/getDevices", { email: email, password: password });

    // Couldn't connect to the myQ API for some reason.
    if((myQDevices?.length === 1) && myQDevices[0] === -1) {

      tdLoginError.innerHTML = "Unable to login to the myQ API.<br>Please check your email address and password.<br><code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>";
      homebridge.hideSpinner();
      return;
    }

    // Save the email and password in our configuration.
    featureOptions.currentConfig[0].email = email;
    featureOptions.currentConfig[0].password = password;
    await homebridge.updatePluginConfig(featureOptions.currentConfig);

    // Create our UI.
    document.getElementById("pageFirstRun").style.display = "none";
    document.getElementById("menuWrapper").style.display = "inline-flex";
    featureOptions.showUI();

    // All done. Let the user interact with us, although in practice, we shouldn't get here.
    // homebridge.hideSpinner();
  });

  document.getElementById("pageFirstRun").style.display = "block";
}

// Show the main plugin configuration tab.
function showSettings () {

  // Show the beachball while we setup.
  homebridge.showSpinner();

  // Create our UI.
  document.getElementById("menuHome").classList.remove("btn-elegant");
  document.getElementById("menuHome").classList.add("btn-primary");
  document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
  document.getElementById("menuFeatureOptions").classList.add("btn-primary");
  document.getElementById("menuSettings").classList.add("btn-elegant");
  document.getElementById("menuSettings").classList.remove("btn-primary");

  document.getElementById("pageSupport").style.display = "none";
  document.getElementById("pageFeatureOptions").style.display = "none";

  homebridge.showSchemaForm();

  // All done. Let the user interact with us.
  homebridge.hideSpinner();
}

// Show the support tab.
function showSupport() {

  // Show the beachball while we setup.
  homebridge.showSpinner();
  homebridge.hideSchemaForm();

  // Create our UI.
  document.getElementById("menuHome").classList.add("btn-elegant");
  document.getElementById("menuHome").classList.remove("btn-primary");
  document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
  document.getElementById("menuFeatureOptions").classList.add("btn-primary");
  document.getElementById("menuSettings").classList.remove("btn-elegant");
  document.getElementById("menuSettings").classList.add("btn-primary");

  document.getElementById("pageSupport").style.display = "block";
  document.getElementById("pageFeatureOptions").style.display = "none";

  // All done. Let the user interact with us.
  homebridge.hideSpinner();
}

// Launch our webUI.
async function launchWebUI() {

  // Retrieve the current plugin configuration.
  featureOptions.currentConfig = await homebridge.getPluginConfig();

  // Add our event listeners to animate the UI.
  menuHome.addEventListener("click", () => showSupport());
  menuFeatureOptions.addEventListener("click", () => featureOptions.showUI());
  menuSettings.addEventListener("click", () => showSettings());

  // If we've got a valid myQ email address and password configured, we launch our feature option UI. Otherwise, we launch our first run UI.
  if(featureOptions.currentConfig.length && featureOptions.currentConfig[0]?.email?.length && featureOptions.currentConfig[0]?.password?.length) {

    document.getElementById("menuWrapper").style.display = "inline-flex";
    featureOptions.showUI();
    return;
  }

  // If we have no configuration, let's create one.
  if(!featureOptions.currentConfig.length) {

    featureOptions.currentConfig.push({ name: "myQ" });
  } else if(!("name" in featureOptions.currentConfig[0])) {

    // If we haven't set the name, let's do so now.
    featureOptions.currentConfig[0].name = "myQ";
  }

  // Update the plugin configuration and launch the first run UI.
  await homebridge.updatePluginConfig(featureOptions.currentConfig);
  showFirstRun();
}

// Fire off our UI, catching errors along the way.
try {

  launchWebUI();
} catch(err) {

  // If we had an error instantiating or updating the UI, notify the user.
  homebridge.toast.error(err.message, "Error");
} finally {

  // Always leave the UI in a usable place for the end user.
  homebridge.hideSpinner();
}


================================================
FILE: homebridge-ui/server.js
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * server.js: homebridge-myq webUI server API.
 *
 * This module is heavily inspired by the homebridge-config-ui-x source code and borrows from both.
 * Thank you oznu for your contributions to the HomeKit world.
 */
"use strict";

import { featureOptionCategories, featureOptions, isOptionEnabled } from "../dist/myq-options.js";
import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
import { myQApi } from "@hjdhjd/myq";
import util from "node:util";

class PluginUiServer extends HomebridgePluginUiServer {

  errorInfo;

  constructor () {
    super();

    this.errorInfo = "";

    // Register getErrorMessage() with the Homebridge server API.
    this.#registerGetErrorMessage();

    // Register getDevices() with the Homebridge server API.
    this.#registerGetDevices();

    // Register getOptions() with the Homebridge server API.
    this.#registerGetOptions();

    this.ready();
  }

  // Register the getErrorMessage() webUI server API endpoint.
  #registerGetErrorMessage() {

    // Return the most recent error message generated by the myQ API.
    this.onRequest("/getErrorMessage", async () => {

      try {

        return this.errorInfo;
      } catch(err) {

        console.log(err);

        // Return nothing if we error out for some reason.
        return "";
      }
    });
  }

  // Register the getDevices() webUI server API endpoint.
  #registerGetDevices() {

    // Return the list of myQ devices.
    this.onRequest("/getDevices", async (myQCredentials) => {

      try {

        const log = {

          debug: (message, parameters) => {},
          error: (message, parameters = []) => {

            // Save the error to inform the user in the webUI.
            if(!!parameters?.[Symbol.iterator]) {

              this.errorInfo = util.format(message, ...parameters);
            } else {

              this.errorInfo = util.format(message, parameters);
            }

            console.error(this.errorInfo);
          },
          info: (message, parameters) => {},
          warn: (message, parameters = []) => {}
        };

        // Connect to the myQ API.
        const myQ = new myQApi(log);

        // Retrieve the list of myQ devices.
        if(!(await myQ.login(myQCredentials.email, myQCredentials.password))) {

          // Either invalid login credentials or an API issue has occurred.
          return [ -1 ];
        }

        // Retrieve the openers and lights we support.
        const openers = myQ.devices.filter(x => x?.device_family.indexOf("garagedoor") !== -1);
        const lights = myQ.devices.filter(x => x?.device_family === "lamp");

        // Adjust our device families to make them more user friendly downstream.
        openers.map(x => x.device_family = "opener");
        openers.map(x => x.hwInfo = myQ.getHwInfo(x.serial_number));
        lights.map(x => x.device_family = "light");

        openers.sort((a, b) => {

          const aCase = (a.name ?? "").toLowerCase();
          const bCase = (b.name ?? "").toLowerCase();

          return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
        });

        lights.sort((a, b) => {

          const aCase = (a.name ?? "").toLowerCase();
          const bCase = (b.name ?? "").toLowerCase();

          return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
        });

        return [ ...openers, ...lights ];
      } catch(err) {

        console.log("Unable to retrieve the list of myQ devices from the myQ API.");
        console.log(err);

        // Return nothing if we error out for some reason.
        return [ -1 ];
      }
    });
  }

  // Register the getOptions() webUI server API endpoint.
  #registerGetOptions() {

    // Return the list of options configured for a given myQ device.
    this.onRequest("/getOptions", async(request) => {

      try {

        const optionSet = {};

        // Loop through all the feature option categories.
        for(const category of featureOptionCategories) {

          optionSet[category.name] = [];

          for(const options of featureOptions[category.name]) {

            options.value = isOptionEnabled(request.configOptions, request.myQDevice, category.name + "." + options.name, options.default);
            optionSet[category.name].push(options);
          }
        }

        return { categories: featureOptionCategories, options: optionSet };

      } catch(err) {

        // Return nothing if we error out for some reason.
        return {};
      }
    });
  }
}

(() => new PluginUiServer())();


================================================
FILE: nodemon.json
================================================
{
  "watch": [
    "src"
  ],
  "ext": "ts",
  "ignore": [],
  "exec": "tsc && homebridge -I -D",
  "signal": "SIGTERM",
  "env": {
    "NODE_OPTIONS": "--trace-warnings"
  }
}

================================================
FILE: package.json
================================================
{
  "name": "homebridge-myq",
  "version": "3.4.4",
  "displayName": "Homebridge myQ",
  "description": "HomeKit integration for myQ enabled devices such as those from LiftMaster and Chamberlain.",
  "author": {
    "name": "HJD",
    "url": "https://github.com/hjdhjd"
  },
  "homepage": "https://github.com/hjdhjd/homebridge-myq#readme",
  "license": "ISC",
  "repository": {
    "type": "git",
    "url": "git://github.com/hjdhjd/homebridge-myq.git"
  },
  "bugs": {
    "url": "https://github.com/hjdhjd/homebridge-myq/issues"
  },
  "keywords": [
    "chamberlain",
    "craftsman",
    "door",
    "garage",
    "garage door",
    "garage door opener",
    "gate",
    "gate opener",
    "homebridge",
    "homebridge-plugin",
    "liftmaster",
    "myq",
    "remote"
  ],
  "type": "module",
  "engines": {
    "homebridge": ">=1.6.0",
    "node": ">=18"
  },
  "scripts": {
    "build": "rimraf ./dist && tsc",
    "clean": "rimraf ./dist",
    "lint": "eslint src/**.ts",
    "postpublish": "npm run clean",
    "prepublishOnly": "npm run lint && npm run build",
    "test": "eslint src/**.ts",
    "watch": "npm run build && npm link && nodemon"
  },
  "main": "dist/index.js",
  "devDependencies": {
    "@types/node": "20.12.7",
    "@types/readable-stream": "4.0.11",
    "@types/ws": "8.5.10",
    "@typescript-eslint/eslint-plugin": "7.7.1",
    "@typescript-eslint/parser": "7.7.1",
    "eslint": "8.57.0",
    "homebridge": "1.8.1",
    "nodemon": "3.1.0",
    "rimraf": "5.0.5",
    "typescript": "5.4.5"
  },
  "dependencies": {
    "@hjdhjd/myq": "7.6.0",
    "@homebridge/plugin-ui-utils": "1.0.3",
    "mqtt": "5.5.4"
  }
}


================================================
FILE: src/index.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * index.ts: homebridge-myq plugin registration.
 */
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
import { API } from "homebridge";
import { myQPlatform } from "./myq-platform.js";

// Register our platform with homebridge.
export default (api: API): void => {

  api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, myQPlatform);
};


================================================
FILE: src/myq-device.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-device.ts: Base class for all myQ devices.
 */
import { API, HAP, PlatformAccessory } from "homebridge";
import { getOptionFloat, getOptionNumber, getOptionValue, isOptionEnabled, myQOptions } from "./myq-options.js";
import { myQApi, myQDevice } from "@hjdhjd/myq";
import { myQPlatform } from "./myq-platform.js";
import util from "node:util";

// Define myQ logging conventions.
interface myQLogging {

  debug: (message: string, ...parameters: unknown[]) => void,
  error: (message: string, ...parameters: unknown[]) => void,
  info: (message: string, ...parameters: unknown[]) => void,
  warn: (message: string, ...parameters: unknown[]) => void
}

// Device-specific options and settings.
interface myQHints {

  automationSwitch: boolean,
  occupancyDuration: number,
  occupancySensor: boolean,
  readOnly: boolean,
  showBatteryInfo: boolean,
  syncNames: boolean
}

export abstract class myQAccessory {

  protected readonly accessory: PlatformAccessory;
  protected readonly api: API;
  protected readonly config: myQOptions;
  protected readonly hap: HAP;
  public hints: myQHints;
  protected readonly log: myQLogging;
  public myQ: myQDevice;
  protected readonly myQApi: myQApi;
  protected readonly platform: myQPlatform;

  // The constructor initializes key variables and calls configureDevice().
  constructor(platform: myQPlatform, accessory: PlatformAccessory, device: myQDevice) {

    this.accessory = accessory;
    this.api = platform.api;
    this.config = platform.config;
    this.hap = this.api.hap;
    this.hints = {} as myQHints;
    this.myQ = device;
    this.myQApi = platform.myQApi;
    this.platform = platform;

    this.log = {

      debug: (message: string, ...parameters: unknown[]): void => platform.debug(util.format(this.name + ": " + message, ...parameters)),
      error: (message: string, ...parameters: unknown[]): void => platform.log.error(util.format(this.name + ": " + message, ...parameters)),
      info: (message: string, ...parameters: unknown[]): void => platform.log.info(util.format(this.name + ": " + message, ...parameters)),
      warn: (message: string, ...parameters: unknown[]): void => platform.log.warn(util.format(this.name + ": " + message, ...parameters))
    };

    this.configureDevice();
  }

  // Configure device-specific settings.
  protected configureHints(): boolean {

    this.hints.syncNames = this.hasFeature("Device.SyncNames");

    return true;
  }

  // All accessories require a configureDevice function. This is where all the accessory-specific configuration and setup happens.
  protected abstract configureDevice(): void;

  // All accessories require an updateState function. This function gets called every few seconds to refresh the accessory state based on the latest information
  // from the myQ API.
  abstract updateState(): boolean;

  // Execute myQ commands.
  protected async command(myQCommand: string): Promise<boolean> {

    if(!this.myQ) {

      this.log.error("Can't find the associated device in the myQ API.");
      return false;
    }

    // Execute the command.
    if(!(await this.myQApi.execute(this.myQ, myQCommand))) {

      return false;
    }

    // Increase the frequency of our polling for state updates to catch any updates from myQ.
    // This will trigger polling at activeRefreshInterval until activeRefreshDuration is hit. If you
    // query the myQ API too quickly, the API won't have had a chance to begin executing our command.
    this.platform.pollOptions.count = 0;
    this.platform.poll(this.config.refreshInterval * -1);

    return true;
  }

  // Configure the device information for HomeKit.
  protected configureInfo(): boolean {

    // Decode our hardware information if we have access to it.
    const hwInfo = this.myQApi.getHwInfo(this.myQ?.serial_number);

    // Update the manufacturer information for this device.
    this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Manufacturer, hwInfo?.brand ?? "Liftmaster");

    // Update the model information for this device.
    this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Model, hwInfo?.product ?? "myQ");

    // Update the serial number for this device.
    if(this.myQ?.serial_number) {

      this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.SerialNumber, this.myQ.serial_number);
    }

    // Set the firmware revision for this device. Fun fact: This firmware information is stored on the gateway not the device.
    const firmwareVersion = this.myQApi.devices.find(x => x.serial_number === this.myQ.parent_device_id)?.state?.firmware_version ?? null;

    if(firmwareVersion) {

      this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, firmwareVersion);
    }

    return true;
  }

  // Utility function to return a floating point configuration parameter on a device.
  public getFeatureFloat(option: string): number | undefined {

    return getOptionFloat(getOptionValue(this.platform.configOptions, this.myQ, option));
  }

  // Utility function to return an integer configuration parameter on a device.
  public getFeatureNumber(option: string): number | undefined {

    return getOptionNumber(getOptionValue(this.platform.configOptions, this.myQ, option));
  }

  // Utility for checking feature options on a device.
  public hasFeature(option: string): boolean {

    return isOptionEnabled(this.platform.configOptions, this.myQ, option, this.platform.featureOptionDefault(option));
  }

  // Name utility function.
  public get name(): string {

    return this.accessory.displayName ?? this.myQ.name;
  }
}


================================================
FILE: src/myq-garagedoor.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-garagedoor.ts: Garage door device class for myQ.
 */
import { MYQ_OBSTRUCTED, MYQ_OBSTRUCTION_ALERT_DURATION, MYQ_OCCUPANCY_DURATION } from "./settings.js";
import { CharacteristicValue } from "homebridge";
import { myQAccessory } from "./myq-device.js";

export class myQGarageDoor extends myQAccessory {

  private batteryDeviceSupport!: boolean;
  private obstructionDetected!: CharacteristicValue;
  private obstructionTimer!: NodeJS.Timeout | null;
  private occupancyTimer!: NodeJS.Timeout | null;

  // Configure a garage door accessory for HomeKit.
  protected configureDevice(): void {

    // Initialize.
    this.batteryDeviceSupport = false;
    this.obstructionDetected = false;
    this.obstructionTimer = null;
    this.occupancyTimer = null;

    // Save our context information before we wipe it out.
    const doorInitialState = this.accessory.context.doorState as CharacteristicValue;

    // Clean out the context object.
    this.accessory.context = {};
    this.accessory.context.doorState = doorInitialState;

    this.configureHints();
    this.configureInfo();
    this.configureGarageDoor();
    this.configureBatteryInfo();
    this.configureSwitch();
    this.configureOccupancySensor();
    this.configureMqtt();
  }

  // Configure device-specific settings for this device.
  protected configureHints(): boolean {

    // Configure our parent's hints.
    super.configureHints();

    // Configure our device-class specific hints.
    this.hints.automationSwitch = this.hasFeature("Opener.Switch");
    this.hints.occupancySensor = this.hasFeature("Opener.OccupancySensor");
    this.hints.occupancyDuration = this.getFeatureNumber("Opener.OccupancySensor.Duration") ?? MYQ_OCCUPANCY_DURATION;
    this.hints.readOnly = this.hasFeature("Opener.ReadOnly");
    this.hints.showBatteryInfo = this.hasFeature("Opener.BatteryInfo");

    return true;
  }

  // Configure the garage door service for HomeKit.
  private configureGarageDoor(): boolean {

    let garageDoorService = this.accessory.getService(this.hap.Service.GarageDoorOpener);

    // Add the garage door opener service to the accessory, if needed.
    if(!garageDoorService) {

      garageDoorService = new this.hap.Service.GarageDoorOpener(this.name);
      this.accessory.addService(garageDoorService);
    }

    // The initial door state when we first startup. The bias functions will help us
    // figure out what to do if we're caught in a tweener state.
    const doorCurrentState = this.doorCurrentStateBias(this.accessory.context.doorState as CharacteristicValue);
    const doorTargetState = this.doorTargetStateBias(doorCurrentState);

    // Set the current and target door states based on our saved state from previous sessions.
    garageDoorService.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, doorCurrentState);
    garageDoorService.updateCharacteristic(this.hap.Characteristic.TargetDoorState, doorTargetState);

    // Handle HomeKit open and close events.
    garageDoorService.getCharacteristic(this.hap.Characteristic.TargetDoorState).onSet((value) => {

      this.setDoorState(value);
    });

    // Inform HomeKit of our current state.
    garageDoorService.getCharacteristic(this.hap.Characteristic.CurrentDoorState).onGet(() => {

      if(this.status === -1) {

        new Error("Unable to determine the current door state.");
      }

      // Return garage door status.
      return this.status;
    });

    // Inform HomeKit on whether we have any obstructions.
    garageDoorService.getCharacteristic(this.hap.Characteristic.ObstructionDetected).onGet(() => {

      // Checking our current door status will force a refresh of any obstruction state.
      this.status;

      // See if we have an obstruction to alert on.
      if(this.obstructionDetected) {

        this.log.info("Obstruction detected.");
      }

      return this.obstructionDetected === true;
    });

    // Add the configured name for this device.
    garageDoorService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);

    // Add our status active characteristic.
    garageDoorService.addOptionalCharacteristic(this.hap.Characteristic.StatusActive);

    // Let HomeKit know that this is the primary service on this accessory.
    garageDoorService.setPrimaryService(true);

    return true;
  }

  // Configure the battery status information for HomeKit.
  private configureBatteryInfo(): boolean {

    // If we don't have a door position sensor, we're done.
    if(!this.myQ?.state || !("dps_low_battery_mode" in this.myQ.state)) {

      return false;
    }

    const doorService = this.accessory.getService(this.hap.Service.GarageDoorOpener);

    // Verify we've already setup the garage door service before trying to configure it.
    if(!doorService) {

      return false;
    }

    // Check to see if we already have a battery service on this accessory.
    let batteryService = this.accessory.getService(this.hap.Service.Battery);

    // We've explicitly disabled the door position sensor, remove the battery service if we have one.
    if(!this.hints.showBatteryInfo) {

      if(batteryService) {

        this.accessory.removeService(batteryService);
      }

      this.log.info("Battery status information will not be displayed in HomeKit.");
      return false;
    }

    // Add the service, if needed.
    if(!batteryService) {

      batteryService = this.accessory.addService(this.hap.Service.Battery);
    }

    // Something's gone wrong, we're done.
    if(!batteryService) {

      this.log.error("Unable to add battery status support.");
      return false;
    }

    batteryService.getCharacteristic(this.hap.Characteristic.StatusLowBattery).onGet(() => {

      return this.dpsBatteryStatus;
    });

    // We only want to configure this once, not on each update. Not the most elegant solution, but it gets the job done.
    this.batteryDeviceSupport = true;
    this.log.info("Door position sensor detected. Enabling battery status support.");

    return true;
  }

  // Configure a switch to automate open and close events in HomeKit beyond what HomeKit might allow for a native garage opener service.
  private configureSwitch(): boolean {

    // Find the switch service, if it exists.
    let switchService = this.accessory.getService(this.hap.Service.Switch);

    // The switch is disabled by default and primarily exists for automation purposes.
    if(!this.hints.automationSwitch) {

      if(switchService) {

        this.accessory.removeService(switchService);
        this.log.info("Disabling automation switch.");
      }

      return false;
    }

    // Add the switch to the opener, if needed.
    if(!switchService) {

      switchService = new this.hap.Service.Switch(this.name + " Automation Switch");

      if(!switchService) {

        this.log.error("Unable to add automation switch.");
        return false;
      }

      this.accessory.addService(switchService);
    }

    // Return the current state of the opener.
    switchService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {

      // We're on if we are in any state other than closed (specifically open or stopped).
      return this.doorCurrentStateBias(this.status) !== this.hap.Characteristic.CurrentDoorState.CLOSED;
    });

    // Open or close the opener.
    switchService.getCharacteristic(this.hap.Characteristic.On)?.onSet((isOn: CharacteristicValue) => {

      // Inform the user.
      this.log.info("Automation switch: %s.", isOn ? "open" : "close" );

      // Send the command.
      if(!this.setDoorState(isOn ? this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED)) {

        // Something went wrong. Let's make sure we revert the switch to it's prior state.
        setTimeout(() => {

          switchService?.updateCharacteristic(this.hap.Characteristic.On, !isOn);
        }, 50);
      }
    });

    // Initialize the switch.
    switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
    switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Automation Switch");
    switchService.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status) !== this.hap.Characteristic.CurrentDoorState.CLOSED);

    this.log.info("Enabling automation switch.");

    return true;
  }

  // Configure the myQ open door occupancy sensor for HomeKit.
  protected configureOccupancySensor(): boolean {

    // Find the occupancy sensor service, if it exists.
    let occupancyService = this.accessory.getService(this.hap.Service.OccupancySensor);

    // The occupancy sensor is disabled by default and primarily exists for automation purposes.
    if(!this.hints.occupancySensor) {

      if(occupancyService) {

        this.accessory.removeService(occupancyService);
        this.log.info("Disabling the open indicator occupancy sensor.");
      }

      return false;
    }

    // We don't have an occupancy sensor, let's add it to the device.
    if(!occupancyService) {

      // We don't have it, add the occupancy sensor to the device.
      occupancyService = new this.hap.Service.OccupancySensor(this.name + " Open");

      if(!occupancyService) {

        this.log.error("Unable to add occupancy sensor.");
        return false;
      }

      this.accessory.addService(occupancyService);
    }

    // Ensure we can configure the name of the occupancy sensor.
    occupancyService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
    occupancyService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.name + " Open");

    // Initialize the state of the occupancy sensor.
    occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
    occupancyService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline);

    occupancyService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => {

      return this.isOnline;
    });

    this.log.info("Enabling the open indicator occupancy sensor. Occupancy will be triggered when the opener has been continuously open for more than %s seconds.",
      this.hints.occupancyDuration);

    return true;
  }

  // Configure MQTT.
  private configureMqtt(): void {

    // Return the current status of the garage door.
    this.platform.mqtt?.subscribe(this.accessory, this.myQ, "garagedoor/get", (message: Buffer) => {

      const value = message?.toString()?.toLowerCase();

      // When we get the right message, we return the list of liveviews.
      if(value !== "true") {
        return;
      }

      // Publish the state of the garage door.
      this.platform.mqtt?.publish(this.accessory, "garagedoor", this.translateDoorState(this.status).toLowerCase());
      this.log.info("Garage door status published via MQTT.");
    });

    // Return the current status of the garage door.
    this.platform.mqtt?.subscribe(this.accessory, this.myQ, "garagedoor/set", (message: Buffer) => {

      const value = message?.toString()?.toLowerCase();
      let targetName;
      let targetState;

      // Figure out what we're setting to.
      switch(value) {

        case "open":

          targetState = this.hap.Characteristic.TargetDoorState.OPEN;
          targetName = "Open";

          break;

        case "close":

          targetState = this.hap.Characteristic.TargetDoorState.CLOSED;
          targetName = "Close";

          break;

        default:

          this.log.error("Unknown door command received via MQTT: %s.", value);

          return;
      }

      // Move the door to the desired position.
      if(this.setDoorState(targetState)) {

        this.log.info("%s command received via MQTT.", targetName);

        return;
      }

      this.log.error("Error executing door command via MQTT: %s.", value);
    });
  }

  // Open or close the garage door.
  private setDoorState(value: CharacteristicValue): boolean {

    if(!this.myQ) {

      this.log.error("Can't find the associated device in the myQ API.");
      return false;
    }

    // If we don't know the door state, we're done.
    if(this.status === -1) {

      return false;
    }

    const actionExisting = this.status === this.hap.Characteristic.CurrentDoorState.OPENING ? "opening" : "closing";
    const actionAttempt = value === this.hap.Characteristic.TargetDoorState.CLOSED ? "close" : "open";

    // If this garage door is read-only, we won't process any requests to set state.
    if(this.hints.readOnly) {

      this.log.info("Unable to %s door. The door has been configured to be read only.", actionAttempt);

      // Tell HomeKit that we haven't in fact changed our state so we don't end up in an inadvertent opening or closing state.
      setTimeout(() => {

        this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.TargetDoorState,
          value === this.hap.Characteristic.TargetDoorState.CLOSED ? this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED);

      }, 0);

      return false;
    }

    // If we are already opening or closing the garage door, we error out. myQ doesn't appear to allow interruptions to an open or close command
    // that is currently executing - it must be allowed to complete its action before accepting a new one.
    if((this.status === this.hap.Characteristic.CurrentDoorState.OPENING) || (this.status === this.hap.Characteristic.CurrentDoorState.CLOSING)) {

      this.log.error("Unable to %s door while currently attempting to complete %s. myQ must complete it's existing action before attempting a new one.",
        actionAttempt, actionExisting);

      return false;
    }

    // Close the garage door.
    if(value === this.hap.Characteristic.TargetDoorState.CLOSED) {

      // HomeKit is asking us to close the garage door, but let's make sure it's not already closed first.
      if(this.status !== this.hap.Characteristic.CurrentDoorState.CLOSED) {

        // We set this to closing instead of closed because we want to show state transitions in HomeKit. In addition, myQ won't immediately execute
        // this command for safety reasons - it enforces a warning tone for a few seconds before it starts the action.
        this.accessory.getService(this.hap.Service.GarageDoorOpener)
          ?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.CLOSING);

        // Execute this command and begin polling myQ for state changes.
        void this.doorCommand(this.hap.Characteristic.TargetDoorState.CLOSED);
      }

      return true;
    }

    // Open the garage door.
    if(value === this.hap.Characteristic.TargetDoorState.OPEN) {

      // HomeKit is informing us to open the door, but we don't want to act if it's already open.
      if(this.status !== this.hap.Characteristic.CurrentDoorState.OPEN) {

        // We set this to opening instad of open because we want to show our state transitions to HomeKit and end users.
        this.accessory.getService(this.hap.Service.GarageDoorOpener)
          ?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.hap.Characteristic.CurrentDoorState.OPENING);

        // Execute this command and begin polling myQ for state changes.
        void this.doorCommand(this.hap.Characteristic.TargetDoorState.OPEN);
      }

      return true;
    }

    // HomeKit has told us something that we don't know how to handle.
    this.log.error("Unknown SET event received: %s.", value);

    return false;
  }

  // Update our HomeKit status.
  public updateState(): boolean {

    // Update our active status.
    this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline);

    // Update our configured name, if requested.
    if(this.hints.syncNames) {

      this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.myQ.name);

      if(this.hints.occupancySensor) {

        this.accessory.getService(this.hap.Service.OccupancySensor)?.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.myQ.name + " Open");
      }
    }

    // Update battery status only if it's supported by the device.
    if(this.batteryDeviceSupport) {

      this.accessory.getService(this.hap.Service.Battery)?.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.dpsBatteryStatus);
    }

    // Trigger our occupancy timer, if configured to do so.
    if(this.hints.occupancySensor) {

      // Set the delay timer if we're in the open state and we don't have one yet.
      if((this.status === this.hap.Characteristic.CurrentDoorState.OPEN) && !this.occupancyTimer) {

        this.occupancyTimer = setTimeout(() => {

          this.accessory.getService(this.hap.Service.OccupancySensor)?.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, true);
          this.log.info("Open state occupancy detected.");
        }, this.hints.occupancyDuration * 1000);
      }

      // If we aren't in non-open state, and we have an occupancy timer, make sure we clear everything out.
      if((this.status !== this.hap.Characteristic.CurrentDoorState.OPEN) && this.occupancyTimer) {

        clearTimeout(this.occupancyTimer);
        this.occupancyTimer = null;

        this.accessory.getService(this.hap.Service.OccupancySensor)?.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
        this.log.info("Open state occupancy no longer detected.");
      }
    }

    // If we can't get our status, we're probably not able to connect to the myQ API.
    if(this.status === -1) {

      this.log.error("Unable to determine the current door state.");
      return false;
    }

    const oldState = this.accessory.context.doorState as CharacteristicValue;

    // If we don't need to update our state in HomeKit, we're done.
    if(oldState === this.status) {

      return true;
    }

    // First, let's save the new door state.
    this.accessory.context.doorState = this.status;

    // We are only going to update the target state if our current state is NOT stopped. If we are stopped, we are at the target state
    // by definition. Unfortunately, the iOS Home app doesn't seem to correctly report a stopped state, although you can find it correctly
    // reported in other HomeKit apps like Eve Home. Finally, we want to ensure we update TargetDoorState before updating CurrentDoorState
    // in order to work around some notification quirks HomeKit occasionally has.
    if(this.status !== this.hap.Characteristic.CurrentDoorState.STOPPED) {

      this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.TargetDoorState, this.doorTargetStateBias(this.status));
    }

    this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, this.status);

    // When we detect any state change, we want to increase our polling resolution to provide timely updates.
    this.platform.pollOptions.count = 0;
    this.platform.poll(this.config.refreshInterval * -1);

    // Inform the user of the state change.
    this.log.info("%s.", this.translateDoorState(this.status));

    // Publish to MQTT, if the user has configured it.
    this.platform.mqtt?.publish(this.accessory, "garagedoor", this.translateDoorState(this.status).toLowerCase());

    // Update our automation switch, if it exists.
    this.accessory.getService(this.hap.Service.Switch)
      ?.updateCharacteristic(this.hap.Characteristic.On, this.doorCurrentStateBias(this.status) !== this.hap.Characteristic.CurrentDoorState.CLOSED);

    return true;
  }

  // Execute garage door commands.
  private async doorCommand(command: CharacteristicValue): Promise<boolean> {

    let myQCommand;
    let myQRevertCurrentState: CharacteristicValue;
    let myQRevertTargetState : CharacteristicValue;

    // Translate the command from HomeKit to myQ.
    switch(command) {

      case this.hap.Characteristic.TargetDoorState.OPEN:

        myQCommand = "open";
        myQRevertCurrentState = this.hap.Characteristic.CurrentDoorState.CLOSED;
        myQRevertTargetState = this.hap.Characteristic.TargetDoorState.CLOSED;
        break;

      case this.hap.Characteristic.TargetDoorState.CLOSED:

        myQCommand = "close";
        myQRevertCurrentState = this.hap.Characteristic.CurrentDoorState.OPEN;
        myQRevertTargetState = this.hap.Characteristic.TargetDoorState.OPEN;
        break;

      default:

        this.log.error("Unknown door command encountered: %s.", command);
        return false;
        break;
    }

    // If the garage opener is offline or our command failed, let's ensure we revert our accessory state.
    if(!this.isOnline || !(await super.command(myQCommand))) {

      if(!this.isOnline) {

        this.log.error("Unable to complete the %s command. The myQ device is currently offline.", myQCommand);
      }

      setTimeout(() => {

        this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.TargetDoorState, myQRevertTargetState);
        this.accessory.getService(this.hap.Service.GarageDoorOpener)?.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, myQRevertCurrentState);
      }, 50);

      return false;
    }

    return true;
  }

  // Utility function to decode HomeKit door state in user-friendly terms.
  private translateDoorState(state: CharacteristicValue): string {

    // HomeKit state decoder ring.
    switch(state) {

      case this.hap.Characteristic.CurrentDoorState.OPEN:

        return "Open";
        break;

      case this.hap.Characteristic.CurrentDoorState.CLOSED:

        return "Closed";
        break;

      case this.hap.Characteristic.CurrentDoorState.OPENING:

        return "Opening";
        break;

      case this.hap.Characteristic.CurrentDoorState.CLOSING:

        return "Closing";
        break;

      case this.hap.Characteristic.CurrentDoorState.STOPPED:

        return "Stopped";
        break;

      case MYQ_OBSTRUCTED:

        return "Obstructed";
        break;

      default:

        return "Unknown";
        break;
    }
  }

  // Utility function to return our bias for what the current door state should be. This is primarily used for our initial bias on startup.
  private doorCurrentStateBias(myQState: CharacteristicValue): CharacteristicValue {

    // Our current door state reflects our opinion on what open or closed means in HomeKit terms. For the obvious states, this is easy.
    // For some of the edge cases, it can be less so. Our north star is that if we are in an obstructed state, we are open.
    switch(myQState) {

      case this.hap.Characteristic.CurrentDoorState.OPEN:
      case this.hap.Characteristic.CurrentDoorState.OPENING:
      case MYQ_OBSTRUCTED:

        return this.hap.Characteristic.CurrentDoorState.OPEN;
        break;

      case this.hap.Characteristic.CurrentDoorState.STOPPED:

        return this.hap.Characteristic.CurrentDoorState.STOPPED;
        break;

      case this.hap.Characteristic.CurrentDoorState.CLOSED:
      case this.hap.Characteristic.CurrentDoorState.CLOSING:
      default:

        return this.hap.Characteristic.CurrentDoorState.CLOSED;
        break;
    }
  }

  // Utility function to return our bias for what the target door state should be.
  private doorTargetStateBias(myQState: CharacteristicValue): CharacteristicValue {

    // We need to be careful with respect to the target state and we need to make some reasonable assumptions about where we intend to end up.
    // If we are opening or closing, our target state needs to be the completion of those actions. If we're stopped or obstructed, we're going
    // to assume the desired target state is to be open, since that is the typical opener behavior, and it's impossible for us to know
    // with reasonable certainty what the original intention of the action was.
    switch(myQState) {

      case this.hap.Characteristic.CurrentDoorState.OPEN:
      case this.hap.Characteristic.CurrentDoorState.OPENING:
      case this.hap.Characteristic.CurrentDoorState.STOPPED:
      case MYQ_OBSTRUCTED:

        return this.hap.Characteristic.TargetDoorState.OPEN;
        break;

      case this.hap.Characteristic.CurrentDoorState.CLOSED:
      case this.hap.Characteristic.CurrentDoorState.CLOSING:
      default:

        return this.hap.Characteristic.TargetDoorState.CLOSED;
        break;
    }
  }

  // Return the status of the door. This function maps myQ door status to HomeKit door status.
  private get status(): CharacteristicValue {

    // Door state cheat sheet.
    //
    // autoreverse is how the myQ API communicated an obstruction...go figure. Unfortunately, it only seems to last the duration of the door reopening (reversal).
    const doorStates: { [index: string]: CharacteristicValue } = {

      autoreverse: MYQ_OBSTRUCTED,
      closed: this.hap.Characteristic.CurrentDoorState.CLOSED,
      closing: this.hap.Characteristic.CurrentDoorState.CLOSING,
      open: this.hap.Characteristic.CurrentDoorState.OPEN,
      opening: this.hap.Characteristic.CurrentDoorState.OPENING,
      stopped: this.hap.Characteristic.CurrentDoorState.STOPPED
    };

    if(!this.myQ) {

      this.log.error("Can't find the associated device in the myQ API.");
      return -1;
    }

    // Retrieve the door state from myQ and map it to HomeKit.
    const myQState = doorStates[this.myQ.state.door_state];

    if(myQState === undefined) {

      this.log.error("Unknown door state encountered: %s.", this.myQ.state.door_state);
      return -1;
    }

    // Obstructed states in the myQ API remain active for a very small period of time. Furthermore, the way
    // HomeKit informs you of an obstructed state is through a status update on the Home app home screen.
    // This ultimately means that an obstructed state has a very small chance of actually being visible to
    // a user unless they happen to be looking at the Home app at the exact moment the obstruction is detected.
    // To ensure the user has a reasonable chance to notice the obstructed state, we will alert a user for up
    // to MYQ_OBSTRUCTION_ALERT_DURATION seconds after the last time we detected an obstruction before clearing
    // out the alert.
    if(myQState === MYQ_OBSTRUCTED) {

      // Clear any other timer that might be out there for obstructions.
      if(this.obstructionTimer) {

        clearTimeout(this.obstructionTimer);
      }

      // Obstruction detected.
      this.obstructionDetected = true;

      const accessory = this.accessory;
      const hap = this.hap;

      // Set the timer for clearing out the obstruction state.
      this.obstructionTimer = setTimeout(() => {

        accessory.getService(hap.Service.GarageDoorOpener)?.updateCharacteristic(hap.Characteristic.ObstructionDetected, this.obstructionDetected);

        this.obstructionDetected = false;
        this.obstructionTimer = null;

        this.log.info("Obstruction cleared.");
      }, MYQ_OBSTRUCTION_ALERT_DURATION * 1000);
    }

    return myQState;
  }

  // Utility to return the battery status of the door sensor, if supported on the device.
  private get dpsBatteryStatus(): CharacteristicValue {

    return this.myQ?.state?.dps_low_battery_mode ?
      this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
  }

  // Online utility function.
  private get isOnline(): boolean {

    return this.myQ?.state.online === true;
  }

  // Name utility function.
  public get name(): string {

    const configuredName = this.accessory.getService(this.hap.Service.GarageDoorOpener)?.getCharacteristic(this.hap.Characteristic.ConfiguredName).value as string;

    return configuredName?.length ? configuredName : super.name;
  }
}


================================================
FILE: src/myq-lamp.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-lamp.ts: Lamp device class for myQ.
 */
import { CharacteristicValue } from "homebridge";
import { myQAccessory } from "./myq-device.js";

export class myQLamp extends myQAccessory {

  private lastUpdate!: number;

  // Configure a lamp accessory for HomeKit.
  protected configureDevice(): void {

    // Save our context information before we wipe it out.
    const lampInitialState = (this.accessory.context.lampState as boolean) === true;

    // Clean out the context object.
    this.accessory.context = {};
    this.accessory.context.lampState = lampInitialState;

    this.configureInfo();
    this.configureLamp();
    this.configureMqtt();
  }

  // Configure the lamp device information for HomeKit.
  protected configureInfo(): boolean {

    // Call our parent first.
    super.configureInfo();

    // Update the model information for this device.
    this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Model, "myQ Light Control");

    // We're done.
    return true;
  }

  // Configure the lightbulb or switch service for HomeKit.
  private configureLamp(): boolean {

    let switchService = this.accessory.getService(this.hap.Service.Switch);

    // Add the switch service to the accessory, if needed.
    if(!switchService) {

      switchService = new this.hap.Service.Switch(this.name);
      this.accessory.addService(switchService);
    }

    switchService.getCharacteristic(this.hap.Characteristic.On).onGet(() => {

      return this.accessory.context.lampState === true;
    });

    switchService.getCharacteristic(this.hap.Characteristic.On).onSet(this.setLampState.bind(this));
    switchService.updateCharacteristic(this.hap.Characteristic.On, (this.accessory.context.lampState as boolean) === true);

    // Add the configured name for this device.
    switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);

    // Add our status active characteristic.
    switchService.addOptionalCharacteristic(this.hap.Characteristic.StatusActive);

    switchService.setPrimaryService(true);

    return true;
  }

  // Configure MQTT.
  private configureMqtt(): void {

    // Return the current status of the lamp device.
    this.platform.mqtt?.subscribe(this.accessory, this.myQ, "lamp/get", (message: Buffer) => {

      const value = message?.toString()?.toLowerCase();

      // When we get the right message, we return the list of liveviews.
      if(value !== "true") {

        return;
      }

      // Publish the state of the lamp.
      this.platform.mqtt?.publish(this.accessory, "lamp", (this.accessory.context.lampState as boolean) ? "on" : "off");
      this.log.info("Lamp status published via MQTT.");
    });

    // Return the current status of the lamp device.
    this.platform.mqtt?.subscribe(this.accessory, this.myQ, "lamp/set", (message: Buffer) => {

      const value = message?.toString()?.toLowerCase();
      let targetName;
      let targetState;

      // Figure out what we're setting to.
      switch(value) {

        case "on":

          targetState = true;
          targetName = "Open";
          break;

        case "off":

          targetState = false;
          targetName = "Close";
          break;

        default:

          this.log.error("Unknown lamp command received via MQTT: %s.", message.toString());
          return;
          break;

      }

      // Move the lamp to the desired position.
      this.log.info("%s command received via MQTT.", targetName);
      this.setLampState(targetState);
    });
  }

  // Turn on or off the lamp.
  private setLampState(value: CharacteristicValue): void {

    if((this.accessory.context.lampState as boolean) !== value) {

      this.log.info("%s.", (value === true) ? "On" : "Off");
    }

    // Save our state and update time.
    this.accessory.context.lampState = value === true;
    this.lastUpdate = Date.now();

    // Execute the command.
    void this.lampCommand(value);
  }

  // Update our HomeKit status.
  public updateState(): boolean {

    // Update our status.
    this.accessory.getService(this.hap.Service.Switch)?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.myQ?.state.online === true);

    const oldState = this.accessory.context.lampState as boolean;
    let myQState = this.lampStatus();

    // If we can't get our status, we're probably not able to connect to the myQ API.
    if(myQState === -1) {

      this.log.error("Unable to determine the current lamp state.");
      return false;
    }

    // Update the state in HomeKit
    if(oldState !== myQState) {

      // Since the myQ takes at least a couple of seconds to respond to state changes, we work around that
      // by checking when the myQ state was last updated and compare it against when we last performed an
      // action. The most recent update within a reasonable amount of time is the one we go with, until myQ catches up.
      const myQLastUpdate = (new Date(this.myQ.state.last_update)).getTime();

      // If our state update is more recent, and in the last five seconds, we'll prioritize it over what myQ says the state is.
      if((this.lastUpdate > myQLastUpdate) && ((this.lastUpdate + 5000) > Date.now())) {

        myQState = oldState;
      } else {

        this.lastUpdate = myQLastUpdate;
      }

      this.accessory.context.lampState = myQState === true;
      this.accessory.getService(this.hap.Service.Switch)?.updateCharacteristic(this.hap.Characteristic.On, (this.accessory.context.lampState as boolean) === true);

      // eslint-disable-next-line camelcase
      this.myQ.state.lamp_state = this.accessory.context.lampState ? "on" : "off";

      // When we detect any state change, we want to increase our polling resolution to provide timely updates.
      this.platform.pollOptions.count = 0;
      this.platform.poll(this.config.refreshInterval * -1);

      this.log.info("%s.", myQState ? "On" : "Off");

      // Publish to MQTT, if the user has configured it.
      this.platform.mqtt?.publish(this.accessory, "lamp", myQState ? "on" : "off");
    }

    // Update our configured name, if requested.
    if(this.hints.syncNames) {

      this.accessory.getService(this.hap.Service.Switch)?.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.myQ.name);
    }

    return true;
  }

  // Return the status of the lamp. This function maps myQ lamp status to HomeKit lamp status.
  private lampStatus(): CharacteristicValue {

    // Lamp state cheat sheet.
    const lampStates: {[index: string]: boolean} = {

      off: false,
      on:  true
    };

    if(!this.myQ) {

      this.log.error("Can't find the associated device in the myQ API.");
      return -1;
    }

    // Retrieve the lamp state from myQ and map it to HomeKit.
    const myQState = lampStates[this.myQ.state.lamp_state];

    if(myQState === undefined) {

      this.log.error("Unknown lamp state encountered: %s.", this.myQ.state.lamp_state);
      return -1;
    }

    return myQState;
  }

  // Execute lamp commands.
  private async lampCommand(command: CharacteristicValue): Promise<boolean> {

    let myQCommand;

    // Translate the command from HomeKit to myQ.
    switch(command) {

      case false:

        myQCommand = "off";
        break;

      case true:

        myQCommand = "on";
        break;

      default:

        this.log.error("Unknown lamp command encountered: %s.", command);
        return false;
        break;
    }

    return super.command(myQCommand);
  }
}


================================================
FILE: src/myq-mqtt.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-mqtt.ts: MQTT connectivity class for myQ.
 */
import { Logging, PlatformAccessory } from "homebridge";
import mqtt, { MqttClient } from "mqtt";
import { MYQ_MQTT_RECONNECT_INTERVAL } from "./settings.js";
import { myQDevice } from "@hjdhjd/myq";
import { myQOptions } from "./myq-options.js";
import { myQPlatform } from "./myq-platform.js";

export class myQMqtt {

  private config: myQOptions;
  private debug: (message: string, ...parameters: unknown[]) => void;
  private isConnected: boolean;
  private log: Logging;
  private mqtt: MqttClient | null;
  private platform: myQPlatform;
  private subscriptions: { [index: string]: (cbBuffer: Buffer) => void };

  constructor(platform: myQPlatform) {

    this.config = platform.config;
    this.debug = platform.debug.bind(platform);
    this.isConnected = false;
    this.log = platform.log;
    this.mqtt = null;
    this.platform = platform;
    this.subscriptions = {};

    if(!this.config.mqttUrl) {

      return;
    }

    this.configure();
  }

  // Connect to the MQTT broker.
  private configure(): void {

    // Try to connect to the MQTT broker and make sure we catch any URL errors.
    try {

      this.mqtt = mqtt.connect(this.config.mqttUrl, { reconnectPeriod: MYQ_MQTT_RECONNECT_INTERVAL * 1000, rejectUnauthorized: false });

    } catch(error) {

      if(error instanceof Error) {

        switch(error.message) {
          case "Missing protocol":

            this.log.error("MQTT Broker: Invalid URL provided: %s.", this.config.mqttUrl);
            break;

          default:

            this.log.error("MQTT Broker: Error: %s.", error.message);
            break;
        }
      }
    }

    // We've been unable to even attempt to connect. It's likely we have a configuration issue - we're done here.
    if(!this.mqtt) {

      return;
    }

    // Notify the user when we connect to the broker.
    this.mqtt.on("connect", () => {

      this.isConnected = true;

      // Magic incantation to redact passwords.
      const redact = /^(?<pre>.*:\/{0,2}.*:)(?<pass>.*)(?<post>@.*)/;

      this.log.info("Connected to MQTT broker: %s (topic: %s).", this.config.mqttUrl.replace(redact, "$<pre>REDACTED$<post>"), this.config.mqttTopic);
    });

    // Notify the user when we've disconnected.
    this.mqtt.on("close", () => {

      if(this.isConnected) {

        this.isConnected = false;

        // Magic incantation to redact passwords.
        const redact = /^(?<pre>.*:\/{0,2}.*:)(?<pass>.*)(?<post>@.*)/;

        this.log.info("Disconnected from MQTT broker: %s", this.config.mqttUrl.replace(redact, "$<pre>REDACTED$<post>"));
      }
    });

    // Process inbound messages and pass it to the right message handler.
    this.mqtt.on("message", (topic: string, message: Buffer) => {

      if(this.subscriptions[topic]) {

        this.subscriptions[topic](message);
      }
    });

    // Notify the user when there's a connectivity error.
    this.mqtt.on("error", (error: Error) => {

      switch((error as NodeJS.ErrnoException).code) {

        case "ECONNREFUSED":

          this.log.error("MQTT Broker: Connection refused (url: %s). Will retry again in %s minute%s.", this.config.mqttUrl,
            MYQ_MQTT_RECONNECT_INTERVAL / 60, MYQ_MQTT_RECONNECT_INTERVAL / 60 > 1 ? "s": "");
          break;

        case "ECONNRESET":

          this.log.error("MQTT Broker: Connection reset (url: %s). Will retry again in %s minute%s.", this.config.mqttUrl,
            MYQ_MQTT_RECONNECT_INTERVAL / 60, MYQ_MQTT_RECONNECT_INTERVAL / 60 > 1 ? "s": "");
          break;

        case "ENOTFOUND":

          this.mqtt?.end(true);
          this.log.error("MQTT Broker: Hostname or IP address not found. (url: %s).", this.config.mqttUrl);
          break;

        default:

          this.log.error("MQTT Broker: %s (url: %s). Will retry again in %s minute%s.", error, this.config.mqttUrl,
            MYQ_MQTT_RECONNECT_INTERVAL / 60, MYQ_MQTT_RECONNECT_INTERVAL / 60 > 1 ? "s": "");
          break;
      }
    });
  }

  // Publish an MQTT event to a broker.
  public publish(accessory: PlatformAccessory, topic: string, message: string): void {

    // No accessory, we're done.
    if(!accessory) {

      return;
    }

    // Expand our topic.
    const expandedTopic = this.expandTopic(topic, accessory);

    // No valid topic returned, we're done.
    if(!expandedTopic) {

      return;
    }

    this.debug("MQTT publish: %s Message: %s.", expandedTopic, message);

    // By default, we publish as: myq/serial/event.
    this.mqtt?.publish(expandedTopic, message);
  }

  // Subscribe to an MQTT topic.
  public subscribe(accessory: PlatformAccessory, device: myQDevice, topic: string, callback: (cbBuffer: Buffer) => void): void {

    // No accessory, we're done.
    if(!accessory) {

      return;
    }

    // Expand our topic.
    const expandedTopic = this.expandTopic(topic, accessory, device);

    // No valid topic returned, we're done.
    if(!expandedTopic) {

      return;
    }

    this.debug("MQTT subscribe: %s.", expandedTopic);

    // Add to our callback list.
    this.subscriptions[expandedTopic] = callback;

    // Tell MQTT we're subscribing to this event.
    // By default, we subscribe as: myq/serial/event.
    this.mqtt?.subscribe(expandedTopic);
  }

  // Expand a topic to a unique, fully formed one.
  private expandTopic(topic: string, accessory: PlatformAccessory, device?: myQDevice) : string | null {

    // No accessory, we're done.
    if(!accessory) {

      return null;
    }

    // Use the myQ device information that's passed to us, or what's already configured on the accessory.
    const myQ = device ?? this.platform.configuredDevices[accessory.UUID]?.myQ;

    return this.config.mqttTopic + "/" + (myQ?.serial_number ?? (myQ?.name ?? "Unknown myQ Device")) + "/" + topic;
  }
}


================================================
FILE: src/myq-options.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-options.ts: Feature option and type definitions for myQ.
 */
import { MYQ_OCCUPANCY_DURATION } from "./settings.js";
import { myQDevice } from "@hjdhjd/myq";

// Plugin configuration options.
export interface myQOptions {

  activeRefreshDuration: number,
  activeRefreshInterval: number,
  debug: boolean,
  email: string,
  mqttTopic: string,
  mqttUrl: string,
  name: string,
  options: string[],
  password: string,
  refreshInterval: number
}

// Feature option categories.
export const featureOptionCategories = [

  { description: "Device feature options.", name: "Device", validFor: [ "all" ] },
  { description: "Opener feature options.", name: "Opener", validFor: [ "opener" ] }
];

/* eslint-disable max-len */
// Individual feature options, broken out by category.
export const featureOptions: { [index: string]: FeatureOption[] } = {

  // Device options.
  "Device": [

    { default: true, description: "Make this device available in HomeKit.", name: "" },
    { default: false, description: "Synchronize the myQ name of this device with HomeKit. Synchronization is one-way only, syncing the device name from myQ to HomeKit.", name: "SyncNames" }
  ],

  // Opener options.
  "Opener": [

    { default: false, description: "Make this opener read-only by ignoring open and close requests from HomeKit.", name: "ReadOnly" },
    { default: true, description: "Display battery status information for myQ door position sensors. You may want to disable this if the myQ status information is incorrectly resulting in a potential notification annoyance in the Home app.", hasProperty: [ "dps_low_battery_mode" ], name: "BatteryInfo" },
    { default: false, description: "Add a switch accessory to control the opener. This can be useful in automation scenarios where you want to work around HomeKit's security restrictions for controlling garage door openers.", name: "Switch" },
    { default: false, description: "Add an occupancy sensor accessory using the open state of the opener to determine occupancy. This can be useful in automation scenarios where you want to trigger an action based on the opener being open for an extended period of time.", name: "OccupancySensor" },
    { default: false, defaultValue: MYQ_OCCUPANCY_DURATION, description: "Duration, in seconds, to wait once the opener has reached the open state before indicating occupancy.", group: "OccupancySensor", name: "OccupancySensor.Duration" }
  ]
};
/* eslint-enable max-len */

export interface FeatureOption {

  default: boolean,           // Default feature option state.
  defaultValue?: number,      // Default value for value-based feature options.
  description: string,        // Description of the feature option.
  group?: string,             // Feature option grouping for related options.
  hasFeature?: string[],      // What hardware-specific features, if any, is this feature option dependent on.
  hasProperty?: string[],     // What myQ JSON property, if any, is this feature option dependent on.
  name: string                // Name of the feature option.
}

// Utility function to let us know whether a feature option should be enabled or not, traversing the scope hierarchy.
export function isOptionEnabled(configOptions: string[], device: myQDevice | null, option = "", defaultReturnValue = true): boolean {

  // There are a couple of ways to enable and disable options. The rules of the road are:
  //
  // 1. Explicitly disabling, or enabling an option on the myQ gateway propogates to all the devices that are managed by that gateway.
  //    Why might you want to do this? Because...
  //
  // 2. Explicitly disabling, or enabling an option on a device by its serial number will always override the above. This means that
  //    it's possible to disable an option for a gateway, and all the devices that are managed by it, and then override that behavior
  //    on a single device that it's managing.

  // Nothing configured - we assume the default return value.
  if(!configOptions.length) {

    return defaultReturnValue;
  }

  const isOptionSet = (checkOption: string, checkSerial: string | undefined = undefined): boolean | undefined => {

    // This regular expression is a bit more intricate than you might think it should be due to the need to ensure we capture values at the very end of the option.
    const optionRegex = new RegExp("^(Enable|Disable)\\." + checkOption + (!checkSerial ? "" : "\\." + checkSerial) + "$", "gi");

    // Get the option value, if we have one.
    for(const entry of configOptions) {

      const regexMatch = optionRegex.exec(entry);

      if(regexMatch) {

        return regexMatch[1].toLowerCase() === "enable";
      }
    }

    return undefined;
  };

  // Check to see if we have a device-level option first.
  if(device?.serial_number) {

    const value = isOptionSet(option, device.serial_number);

    if(value !== undefined) {

      return value;
    }
  }

  // Finally, we check for a global-level value.
  const value = isOptionSet(option);

  if(value !== undefined) {

    return value;
  }

  // The option hasn't been set at any scope, return our default value.
  return defaultReturnValue;
}

// Utility function to return a value-based feature option for a myQ device.
export function getOptionValue(configOptions: string[], device: myQDevice | null, option: string): string | undefined {

  // Nothing configured - we assume there's nothing.
  if(!configOptions.length || !option) {

    return undefined;
  }

  const getValue = (checkOption: string, checkSerial: string | undefined = undefined): string | undefined => {

    // This regular expression is a bit more intricate than you might think it should be due to the need to ensure we capture values at the very end of the option.
    const optionRegex = new RegExp("^Enable\\." + checkOption + (!checkSerial ? "" : "\\." + checkSerial) + "\\.([^\\.]+)$", "gi");

    // Get the option value, if we have one.
    for(const entry of configOptions) {

      const regexMatch = optionRegex.exec(entry);

      if(regexMatch) {

        return regexMatch[1];
      }
    }

    return undefined;
  };

  // Check to see if we have a device-level value first.
  if(device?.serial_number) {

    const value = getValue(option, device.serial_number);

    if(value) {

      return value;
    }
  }

  // Finally, we check for a global-level value.
  return getValue(option);
}

// Utility function to parse and return a numeric configuration parameter.
function parseOptionNumeric(optionValue: string | undefined, convert: (value: string) => number): number | undefined {

  // We don't have the option configured -- we're done.
  if(optionValue === undefined) {

    return undefined;
  }

  // Convert it to a number, if needed.
  const convertedValue = convert(optionValue);

  // Let's validate to make sure it's really a number.
  if(isNaN(convertedValue) || (convertedValue < 0)) {

    return undefined;
  }

  // Return the value.
  return convertedValue;
}

// Utility function to return a floating point configuration parameter.
export function getOptionFloat(optionValue: string | undefined): number | undefined {

  return parseOptionNumeric(optionValue, (value: string) => {

    return parseFloat(value);
  });
}

// Utility function to return an integer configuration parameter on a device.
export function getOptionNumber(optionValue: string | undefined): number | undefined {

  return parseOptionNumeric(optionValue, (value: string) => {

    return parseInt(value);
  });
}


================================================
FILE: src/myq-platform.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * myq-platform.ts: homebridge-myq platform class.
 */
import { API, APIEvent, DynamicPlatformPlugin, HAP, Logging, PlatformAccessory, PlatformConfig } from "homebridge";
import { MYQ_ACTIVE_DEVICE_REFRESH_DURATION, MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL, MYQ_DEVICE_REFRESH_INTERVAL, MYQ_MQTT_TOPIC,
  PLATFORM_NAME, PLUGIN_NAME } from "./settings.js";
import { featureOptionCategories, featureOptions, isOptionEnabled, myQOptions } from "./myq-options.js";
import { myQAccessory } from "./myq-device.js";
import { myQApi } from "@hjdhjd/myq";
import { myQGarageDoor } from "./myq-garagedoor.js";
import { myQLamp } from "./myq-lamp.js";
import { myQMqtt } from "./myq-mqtt.js";
import util from "node:util";

interface myQPollInterface {

  count: number,
  maxCount: number,
}

export class myQPlatform implements DynamicPlatformPlugin {

  private readonly accessories: PlatformAccessory[];
  public readonly api: API;
  private featureOptionDefaults: { [index: string]: boolean };
  public config!: myQOptions;
  public readonly configOptions: string[];
  public readonly configuredDevices: { [index: string]: myQAccessory };
  public readonly hap: HAP;
  public readonly log: Logging;
  public readonly mqtt!: myQMqtt;
  public readonly myQApi!: myQApi;
  private pollingTimer!: NodeJS.Timeout;
  public readonly pollOptions!: myQPollInterface;
  private unsupportedDevices: { [index: string]: boolean };

  constructor(log: Logging, config: PlatformConfig, api: API) {

    this.accessories = [];
    this.api = api;
    this.configOptions = [];
    this.configuredDevices = {};
    this.featureOptionDefaults = {};
    this.hap = api.hap;
    this.log = log;
    this.log.debug = this.debug.bind(this);
    this.unsupportedDevices = {};

    // Inform users this plugin has been retired...for now.
    this.log.info("Unfortunately, this plugin is being retired for the time being. Liftmaster/Chamberlain has decided to eliminate access to their API to the open " +
      "source community. Until this situation changes, homebridge-myq will be retired. For those in the Liftmaster/Chamberlain ecosystem, I recommend you try my " +
      "homebridge-ratgdo plugin. Ratgdo is an open source hardware solution that provides all the same functionality as myQ, and more.");

    this.log.error("Unfortunately, this plugin is being retired for the time being. Liftmaster/Chamberlain has decided to eliminate access to their API to the open " +
      "source community. Until this situation changes, homebridge-myq will be retired. For those in the Liftmaster/Chamberlain ecosystem, I recommend you try my " +
      "homebridge-ratgdo plugin. Ratgdo is an open source hardware solution that provides all the same functionality as myQ, and more.");

    return;

    // Build our list of default values for our feature options.
    for(const category of featureOptionCategories) {

      for(const options of featureOptions[category.name]) {

        this.featureOptionDefaults[(category.name + (options.name.length ? "." + options.name : "")).toLowerCase()] = options.default;
      }
    }

    // We can't start without being configured.
    if(!config) {

      return;
    }

    this.config = {

      activeRefreshDuration: "activeRefreshDuration" in config ? parseInt(config.activeRefreshDuration as string) : MYQ_ACTIVE_DEVICE_REFRESH_DURATION,
      activeRefreshInterval: "activeRefreshInterval" in config ? parseInt(config.activeRefreshInterval as string) : MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL,
      debug: config.debug === true,
      email: config.email as string,
      mqttTopic: config.mqttTopic as string ?? MYQ_MQTT_TOPIC,
      mqttUrl: config.mqttUrl as string,
      name: config.name as string,
      options: config.options as string[],
      password: config.password as string,
      refreshInterval: "refreshInterval" in config ? parseInt(config.refreshInterval as string) : MYQ_DEVICE_REFRESH_INTERVAL
    };

    // We need login credentials or we're not starting.
    if(!this.config.email || !this.config.password) {

      this.log.error("No myQ login credentials configured.");
      return;
    }

    // Make sure the active refresh duration is reasonable.
    if((this.config.activeRefreshDuration > 300) || (this.config.activeRefreshDuration !== this.config.activeRefreshDuration)) {

      this.log.info("Adjusting myQ API normal refresh duration from %s to %s." +
        " Setting too high of a normal refresh duration is strongly discouraged due to myQ occasionally blocking accounts who overtax the myQ API.",
      this.config.activeRefreshDuration, MYQ_ACTIVE_DEVICE_REFRESH_DURATION);

      this.config.activeRefreshDuration = MYQ_ACTIVE_DEVICE_REFRESH_DURATION;

    }

    // Make sure the active refresh interval is reasonable.
    if((this.config.activeRefreshInterval < 2) || (this.config.activeRefreshInterval !== this.config.activeRefreshInterval)) {

      this.log.info("Adjusting myQ API active refresh interval from %s to %s." +
        " Setting too short of an active refresh interval is strongly discouraged due to myQ occasionally blocking accounts who overtax the myQ API.",
      this.config.activeRefreshInterval, MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL);

      this.config.activeRefreshInterval = MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL;

    }

    // If we have feature options, put them into their own array, upper-cased for future reference.
    if(this.config.options) {

      for(const featureOption of this.config.options) {

        this.configOptions.push(featureOption.toLowerCase());
      }
    }

    // Make sure the refresh interval is reasonable.
    if((this.config.refreshInterval < 5) || (this.config.refreshInterval !== this.config.refreshInterval)) {

      this.log.info("Adjusting myQ API refresh interval from %s to %s seconds." +
        " Even at this value, you are strongly encouraged to increase this to at least 10 seconds due to myQ occasionally blocking accounts who overtax the myQ API.",
      this.config.refreshInterval, MYQ_DEVICE_REFRESH_INTERVAL);

      this.config.refreshInterval = MYQ_DEVICE_REFRESH_INTERVAL;

    }

    this.debug("Debug logging on. Expect a lot of data.");

    this.pollOptions = {

      count: this.config.activeRefreshDuration / this.config.activeRefreshInterval,
      maxCount: this.config.activeRefreshDuration / this.config.activeRefreshInterval
    };

    // Initialize our connection to the myQ API.
    this.myQApi = new myQApi(this.log);

    // Create an MQTT connection, if needed.
    if(!this.mqtt && this.config.mqttUrl) {

      this.mqtt = new myQMqtt(this);
    }

    // Avoid a prospective race condition by waiting to begin our polling until Homebridge is done loading all the cached accessories it knows about, and calling
    // configureAccessory() on each.
    //
    // Fire off our polling, with an immediate status refresh to begin with to provide us that responsive feeling.
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    api.on(APIEvent.DID_FINISH_LAUNCHING, this.login.bind(this));
  }

  // This gets called when homebridge restores cached accessories at startup. We intentionally avoid doing anything significant here, and save all that logic
  // for device discovery.
  public configureAccessory(accessory: PlatformAccessory): void {

    // Add this to the accessory array so we can track it.
    this.accessories.push(accessory);
  }

  private async login(): Promise<boolean> {

    // Whether we login successfully or not here, we're going to continue forward. The API isn't always reliable and simply stopping at this stage would leave users
    // who might have valid credentials unable to access the API.
    await this.myQApi.login(this.config.email, this.config.password);

    // Fire off our polling, with an immediate status refresh to begin with to provide us that responsive feeling.
    this.poll(this.config.refreshInterval * -1);

    return true;
  }

  // Discover new myQ devices and sync existing ones with the myQ API.
  private discoverAndSyncAccessories(): boolean {

    // Iterate through the list of devices that myQ has returned and sync them with what we show HomeKit.
    for(const device of this.myQApi.devices) {

      // If we have no serial number or device family, something is wrong.
      if(!device.serial_number || !device.device_family) {

        continue;
      }

      // We are only interested in garage door openers. Perhaps more types in the future.
      switch(true) {

        case (device.device_family.indexOf("garagedoor") !== -1):
        case (device.device_family === "lamp"):

          // We have a known device type. One of:
          //   - garage door.
          //   - lamp.
          break;

        default:

          // Unless we are debugging device discovery, ignore any gateways.
          // These are typically gateways, hubs, etc. that shouldn't be causing us to alert anyway.
          if(!this.config.debug && device.device_family === "gateway") {

            continue;
          }

          // If we've already informed the user about this one, we're done.
          if(this.unsupportedDevices[device.serial_number]) {

            continue;
          }

          // Notify the user we see this device, but we aren't adding it to HomeKit.
          this.unsupportedDevices[device.serial_number] = true;

          this.log.info("myQ device family '%s' is not currently supported, ignoring: %s.", device.device_family, this.myQApi.getDeviceName(device));
          continue;

          break;
      }

      // Exclude or include certain openers based on configuration parameters.
      if(!isOptionEnabled(this.configOptions, device, "Device", this.featureOptionDefault("Device"))) {

        continue;
      }

      // Generate this device's unique identifier.
      const uuid = this.hap.uuid.generate(device.serial_number);

      // See if we already know about this accessory or if it's truly new. If it is new, add it to HomeKit.
      let accessory = this.accessories.find(x => x.UUID === uuid);

      if(!accessory) {

        accessory = new this.api.platformAccessory(device.name, uuid);

        this.log.info("%s: Adding %s device to HomeKit: %s.", device.name, device.device_family, this.myQApi.getDeviceName(device));

        // Register this accessory with homebridge and add it to the accessory array so we can track it.
        this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
        this.accessories.push(accessory);
      }

      // If we've already configured this accessory, update it's state and we're done here.
      if(this.configuredDevices[accessory.UUID]) {

        this.configuredDevices[accessory.UUID].myQ = device;
        continue;
      }

      // Eventually switch on multiple types of myQ devices. For now, it's garage doors only...
      switch(true) {

        case (device.device_family.indexOf("garagedoor") !== -1):

          // We have a garage door.
          this.configuredDevices[accessory.UUID] = new myQGarageDoor(this, accessory, device);
          break;

        case (device.device_family === "lamp"):

          // We have a lamp.
          this.configuredDevices[accessory.UUID] = new myQLamp(this, accessory, device);
          break;

        default:

          // We should never get here.
          this.log.error("Unknown device type detected: %s.", device.device_family);
          break;
      }

      // Refresh the accessory cache with these values.
      this.api.updatePlatformAccessories([accessory]);
    }

    // Remove myQ devices that are no longer found in the myQ API, but we still have in HomeKit.
    for(const oldAccessory of this.accessories) {

      const device = this.configuredDevices[oldAccessory.UUID];

      // We found this accessory in myQ. Figure out if we really want to see it in HomeKit.
      if(device?.hasFeature("Device")) {

        continue;
      }

      this.log.info("%s: Removing myQ device from HomeKit.", device?.name ?? oldAccessory.displayName);

      delete this.configuredDevices[oldAccessory.UUID];
      this.accessories.splice(this.accessories.indexOf(oldAccessory), 1);
      this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [oldAccessory]);
    }

    return true;
  }

  // Update HomeKit with the latest status from myQ.
  private async updateAccessories(): Promise<boolean> {

    // Refresh the full device list from the myQ API.
    if(!(await this.myQApi.refreshDevices())) {

      return false;
    }

    // Sync myQ status and check for any new or removed accessories.
    this.discoverAndSyncAccessories();

    // Iterate through our accessories and update its status with the corresponding myQ status.
    for(const key in this.configuredDevices) {

      this.configuredDevices[key].updateState();
    }

    return true;
  }

  // Periodically poll the myQ API for status.
  public poll(delay = 0): void {

    let refresh = this.config.refreshInterval + delay;

    // Clear the last polling interval out.
    clearTimeout(this.pollingTimer);

    // Normally, count just increments on each call. However, when we want to increase our polling frequency, count is set to 0 (elsewhere in the plugin) to put us in a
    // more frequent polling mode. This is determined by the values configured for activeRefreshDuration and activeRefreshInterval which specify the maximum length of
    // time for this increased polling frequency (activeRefreshDuration) and the actual frequency of each update (activeRefreshInterval).
    if(this.pollOptions.count < this.pollOptions.maxCount) {

      refresh = this.config.activeRefreshInterval + delay;
      this.pollOptions.count++;
    }

    // Setup periodic update with our polling interval.
    this.pollingTimer = setTimeout(() => {

      void (async (): Promise<void> => {

        // Refresh our myQ information and gracefully handle myQ errors.
        if(!(await this.updateAccessories())) {

          this.pollOptions.count = this.pollOptions.maxCount - 1;
        }

        // Fire off the next polling interval.
        this.poll();

      })();

    }, refresh * 1000);

  }

  // Utility to return the default value for a feature option.
  public featureOptionDefault(option: string): boolean {

    const defaultValue = this.featureOptionDefaults[option.toLowerCase()];

    // If it's unknown to us, assume it's true.
    if(defaultValue === undefined) {

      return true;
    }

    return defaultValue;
  }

  // Utility for debug logging.
  public debug(message: string, ...parameters: unknown[]): void {

    if(this.config.debug) {

      this.log.info(util.format(message, ...parameters));
    }
  }
}


================================================
FILE: src/settings.ts
================================================
/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
 *
 * settings.ts: Settings and constants for homebridge-myq.
 */
// How often, in seconds, should we poll the myQ API for updates about myQ devices and their states.
export const MYQ_DEVICE_REFRESH_INTERVAL = 12;

// How often, in seconds, should we poll the myQ API during active state changes in myQ devices, like garage doors.
export const MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL = 3;

// How long, in seconds, should we continue to actively poll myQ device state changes.
export const MYQ_ACTIVE_DEVICE_REFRESH_DURATION = 60 * 5;

// How long, in seconds, should we alert a user to an obstruction.
export const MYQ_OBSTRUCTION_ALERT_DURATION = 30;

// Default duration, in seconds, before triggering occupancy
Download .txt
gitextract_upv4e1ze/

├── .eslintrc.json
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── config.yml
│   │   └── feature_request.md
│   ├── auto-merge.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci.yml
│       ├── dependabot-automerge.yml
│       ├── issue-stale.yml
│       ├── issue-validate.yml
│       └── lock-threads.yml
├── .gitignore
├── .npmignore
├── CODE-OF-CONDUCT.md
├── LICENSE.md
├── README.md
├── config.schema.json
├── docs/
│   ├── AdvancedOptions.md
│   ├── Changelog.md
│   ├── FeatureOptions.md
│   └── MQTT.md
├── homebridge-ui/
│   ├── public/
│   │   ├── index.html
│   │   ├── lib/
│   │   │   └── featureoptions.mjs
│   │   ├── myq-featureoptions.mjs
│   │   └── ui.mjs
│   └── server.js
├── nodemon.json
├── package.json
├── src/
│   ├── index.ts
│   ├── myq-device.ts
│   ├── myq-garagedoor.ts
│   ├── myq-lamp.ts
│   ├── myq-mqtt.ts
│   ├── myq-options.ts
│   ├── myq-platform.ts
│   └── settings.ts
└── tsconfig.json
Download .txt
SYMBOL INDEX (96 symbols across 11 files)

FILE: homebridge-ui/public/lib/featureoptions.mjs
  class FeatureOptions (line 7) | class FeatureOptions {
    method constructor (line 15) | constructor() {
    method showUI (line 24) | async showUI() {
    method isOptionSet (line 28) | isOptionSet(featureOption, deviceMac) {
    method isGlobalOptionEnabled (line 35) | isGlobalOptionEnabled(featureOption, defaultState) {
    method isDeviceOptionEnabled (line 46) | isDeviceOptionEnabled(featureOption, mac, defaultState) {
    method isOptionValueSet (line 63) | isOptionValueSet(featureOption, deviceMac) {
    method getOptionValue (line 71) | getOptionValue(featureOption, deviceMac) {
    method isOptionEnabled (line 90) | isOptionEnabled(featureOption, deviceMac) {
    method optionScope (line 120) | optionScope(featureOption, deviceMac, defaultState, isOptionValue = fa...
    method optionScopeColor (line 174) | optionScopeColor(featureOption, deviceMac, defaultState, isOptionValue) {

FILE: homebridge-ui/public/myq-featureoptions.mjs
  class myQFeatureOptions (line 9) | class myQFeatureOptions extends FeatureOptions {
    method constructor (line 26) | constructor() {
    method showUI (line 38) | async showUI() {
    method #showDevices (line 127) | async #showDevices(isGlobal) {
    method #showDeviceInfo (line 235) | async #showDeviceInfo(deviceId) {
    method #updateConfigOptions (line 655) | #updateConfigOptions(newConfig) {

FILE: homebridge-ui/public/ui.mjs
  function showFirstRun (line 13) | async function showFirstRun () {
  function showSettings (line 81) | function showSettings () {
  function showSupport (line 104) | function showSupport() {
  function launchWebUI (line 126) | async function launchWebUI() {

FILE: homebridge-ui/server.js
  class PluginUiServer (line 15) | class PluginUiServer extends HomebridgePluginUiServer {
    method constructor (line 19) | constructor () {
    method #registerGetErrorMessage (line 37) | #registerGetErrorMessage() {
    method #registerGetDevices (line 56) | #registerGetDevices() {
    method #registerGetOptions (line 131) | #registerGetOptions() {

FILE: src/myq-device.ts
  type myQLogging (line 12) | interface myQLogging {
  type myQHints (line 21) | interface myQHints {
  method constructor (line 44) | constructor(platform: myQPlatform, accessory: PlatformAccessory, device:...
  method configureHints (line 67) | protected configureHints(): boolean {
  method command (line 82) | protected async command(myQCommand: string): Promise<boolean> {
  method configureInfo (line 106) | protected configureInfo(): boolean {
  method getFeatureFloat (line 135) | public getFeatureFloat(option: string): number | undefined {
  method getFeatureNumber (line 141) | public getFeatureNumber(option: string): number | undefined {
  method hasFeature (line 147) | public hasFeature(option: string): boolean {
  method name (line 153) | public get name(): string {

FILE: src/myq-garagedoor.ts
  class myQGarageDoor (line 9) | class myQGarageDoor extends myQAccessory {
    method configureDevice (line 17) | protected configureDevice(): void {
    method configureHints (line 42) | protected configureHints(): boolean {
    method configureGarageDoor (line 58) | private configureGarageDoor(): boolean {
    method configureBatteryInfo (line 124) | private configureBatteryInfo(): boolean {
    method configureSwitch (line 181) | private configureSwitch(): boolean {
    method configureOccupancySensor (line 247) | protected configureOccupancySensor(): boolean {
    method configureMqtt (line 299) | private configureMqtt(): void {
    method setDoorState (line 360) | private setDoorState(value: CharacteristicValue): boolean {
    method updateState (line 445) | public updateState(): boolean {
    method doorCommand (line 538) | private async doorCommand(command: CharacteristicValue): Promise<boole...
    method translateDoorState (line 589) | private translateDoorState(state: CharacteristicValue): string {
    method doorCurrentStateBias (line 632) | private doorCurrentStateBias(myQState: CharacteristicValue): Character...
    method doorTargetStateBias (line 660) | private doorTargetStateBias(myQState: CharacteristicValue): Characteri...
    method status (line 686) | private get status(): CharacteristicValue {
    method dpsBatteryStatus (line 753) | private get dpsBatteryStatus(): CharacteristicValue {
    method isOnline (line 760) | private get isOnline(): boolean {
    method name (line 766) | public get name(): string {

FILE: src/myq-lamp.ts
  class myQLamp (line 8) | class myQLamp extends myQAccessory {
    method configureDevice (line 13) | protected configureDevice(): void {
    method configureInfo (line 28) | protected configureInfo(): boolean {
    method configureLamp (line 41) | private configureLamp(): boolean {
    method configureMqtt (line 72) | private configureMqtt(): void {
    method setLampState (line 127) | private setLampState(value: CharacteristicValue): void {
    method updateState (line 143) | public updateState(): boolean {
    method lampStatus (line 201) | private lampStatus(): CharacteristicValue {
    method lampCommand (line 229) | private async lampCommand(command: CharacteristicValue): Promise<boole...

FILE: src/myq-mqtt.ts
  class myQMqtt (line 12) | class myQMqtt {
    method constructor (line 22) | constructor(platform: myQPlatform) {
    method configure (line 41) | private configure(): void {
    method publish (line 139) | public publish(accessory: PlatformAccessory, topic: string, message: s...
    method subscribe (line 163) | public subscribe(accessory: PlatformAccessory, device: myQDevice, topi...
    method expandTopic (line 191) | private expandTopic(topic: string, accessory: PlatformAccessory, devic...

FILE: src/myq-options.ts
  type myQOptions (line 9) | interface myQOptions {
  type FeatureOption (line 53) | interface FeatureOption {
  function isOptionEnabled (line 65) | function isOptionEnabled(configOptions: string[], device: myQDevice | nu...
  function getOptionValue (line 125) | function getOptionValue(configOptions: string[], device: myQDevice | nul...
  function parseOptionNumeric (line 168) | function parseOptionNumeric(optionValue: string | undefined, convert: (v...
  function getOptionFloat (line 190) | function getOptionFloat(optionValue: string | undefined): number | undef...
  function getOptionNumber (line 199) | function getOptionNumber(optionValue: string | undefined): number | unde...

FILE: src/myq-platform.ts
  type myQPollInterface (line 16) | interface myQPollInterface {
  class myQPlatform (line 22) | class myQPlatform implements DynamicPlatformPlugin {
    method constructor (line 38) | constructor(log: Logging, config: PlatformConfig, api: API) {
    method configureAccessory (line 166) | public configureAccessory(accessory: PlatformAccessory): void {
    method login (line 172) | private async login(): Promise<boolean> {
    method discoverAndSyncAccessories (line 185) | private discoverAndSyncAccessories(): boolean {
    method updateAccessories (line 309) | private async updateAccessories(): Promise<boolean> {
    method poll (line 330) | public poll(delay = 0): void {
    method featureOptionDefault (line 367) | public featureOptionDefault(option: string): boolean {
    method debug (line 381) | public debug(message: string, ...parameters: unknown[]): void {

FILE: src/settings.ts
  constant MYQ_DEVICE_REFRESH_INTERVAL (line 6) | const MYQ_DEVICE_REFRESH_INTERVAL = 12;
  constant MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL (line 9) | const MYQ_ACTIVE_DEVICE_REFRESH_INTERVAL = 3;
  constant MYQ_ACTIVE_DEVICE_REFRESH_DURATION (line 12) | const MYQ_ACTIVE_DEVICE_REFRESH_DURATION = 60 * 5;
  constant MYQ_OBSTRUCTION_ALERT_DURATION (line 15) | const MYQ_OBSTRUCTION_ALERT_DURATION = 30;
  constant MYQ_OCCUPANCY_DURATION (line 18) | const MYQ_OCCUPANCY_DURATION = 300;
  constant MYQ_MQTT_RECONNECT_INTERVAL (line 21) | const MYQ_MQTT_RECONNECT_INTERVAL = 60;
  constant MYQ_MQTT_TOPIC (line 24) | const MYQ_MQTT_TOPIC = "myq";
  constant MYQ_OBSTRUCTED (line 27) | const MYQ_OBSTRUCTED = 8675309;
  constant PLATFORM_NAME (line 30) | const PLATFORM_NAME = "myQ";
  constant PLUGIN_NAME (line 33) | const PLUGIN_NAME = "homebridge-myq";
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (205K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 1401,
    "preview": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/eslin"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 1326,
    "preview": "---\nname: Support Request\nabout: Report a bug or request help. Please read the documentation first before creating a sup"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 168,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Homebridge Discord Community\n    url: https://discord.gg/QXqfHEW\n  "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 735,
    "preview": "---\nname: Feature Request\nabout: Suggest an idea for an enhancement.\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n*"
  },
  {
    "path": ".github/auto-merge.yml",
    "chars": 171,
    "preview": "# Merge all dependencies as long within ${TARGET} scope (defined in workflows/dependabot-automerge.yml).\n#\n- match:\n    "
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 971,
    "preview": "# Query daily for npm dependency updates.\n#\nversion: 2\n\nupdates:\n\n  # Enable version updates for github-actions.\n  - pac"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2360,
    "preview": "# Continuous integration - validate builds when commits are made, and publish when releases are created.\n#\nname: \"Contin"
  },
  {
    "path": ".github/workflows/dependabot-automerge.yml",
    "chars": 414,
    "preview": "# Automerge dependency updates identified by dependabot.\n#\nname: Automerge Dependabot Version Updates\n\non:\n  pull_reques"
  },
  {
    "path": ".github/workflows/issue-stale.yml",
    "chars": 1040,
    "preview": "# Close stale issues after a defined period of time.\n#\nname: Close Stale Issues\n\non:\n  issues:\n    types: [reopened]\n  s"
  },
  {
    "path": ".github/workflows/issue-validate.yml",
    "chars": 845,
    "preview": "# Close issues that don't conform to the issue templates.\n#\nname: Close Non-Conforming Issues\n\non:\n  issues:\n    types: "
  },
  {
    "path": ".github/workflows/lock-threads.yml",
    "chars": 810,
    "preview": "name: 'Lock Threads'\n\non:\n  schedule:\n    - cron: '0 1 * * *'\n\npermissions:\n  issues: write\n  pull-requests: write\n\nconc"
  },
  {
    "path": ".gitignore",
    "chars": 1929,
    "preview": "# Ignore compiled code\ndist\n\n# Ignore npmrc.\n.npmrc\n\n# Ignore macOS attribute files.\n.DS_Store\n\n# ------------- Defaults"
  },
  {
    "path": ".npmignore",
    "chars": 144,
    "preview": "# Ignore everything by default.\n*\n\n# Include the following.\n!LICENSE.md\n!README.md\n!config.schema.json\n!dist/**\n!homebri"
  },
  {
    "path": "CODE-OF-CONDUCT.md",
    "chars": 1401,
    "preview": "# Code of Conduct\n\nBy interacting with this GitHub repository, you agree that you'll follow this code of conduct.\n\n### I"
  },
  {
    "path": "LICENSE.md",
    "chars": 830,
    "preview": "Internet Systems Consortium license\n===================================\n\nCopyright (c) `2017-2023`, `HJD https://github."
  },
  {
    "path": "README.md",
    "chars": 15023,
    "preview": "<SPAN ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n<DIV ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n\n[![homebridge-myq: Nativ"
  },
  {
    "path": "config.schema.json",
    "chars": 5077,
    "preview": "{\n  \"pluginAlias\": \"myQ\",\n  \"pluginType\": \"platform\",\n  \"singular\": true,\n  \"customUi\": true,\n  \"headerDisplay\": \"[homeb"
  },
  {
    "path": "docs/AdvancedOptions.md",
    "chars": 6590,
    "preview": "<SPAN ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n<DIV ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n\n[![homebridge-myq: Nativ"
  },
  {
    "path": "docs/Changelog.md",
    "chars": 15230,
    "preview": "# Changelog\n\nAll notable changes to this project will be documented in this file. This project uses [semantic versioning"
  },
  {
    "path": "docs/FeatureOptions.md",
    "chars": 10170,
    "preview": "<SPAN ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n<DIV ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n\n[![homebridge-myq: Nativ"
  },
  {
    "path": "docs/MQTT.md",
    "chars": 7443,
    "preview": "<SPAN ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n<DIV ALIGN=\"CENTER\" STYLE=\"text-align:center\">\n\n[![homebridge-myq: Nativ"
  },
  {
    "path": "homebridge-ui/public/index.html",
    "chars": 6629,
    "preview": "<p class=\"text-center\">\n  <img src=\"https://raw.githubusercontent.com/hjdhjd/homebridge-myq/main/homebridge-myq.svg\" alt"
  },
  {
    "path": "homebridge-ui/public/lib/featureoptions.mjs",
    "chars": 5420,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * featureoptions.mjs: Feature optio"
  },
  {
    "path": "homebridge-ui/public/myq-featureoptions.mjs",
    "chars": 25395,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-featureoptions.mjs: myQ featu"
  },
  {
    "path": "homebridge-ui/public/ui.mjs",
    "chars": 6053,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * ui.mjs: myQ webUI.\n */\n\"use stric"
  },
  {
    "path": "homebridge-ui/server.js",
    "chars": 4615,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * server.js: homebridge-myq webUI s"
  },
  {
    "path": "nodemon.json",
    "chars": 176,
    "preview": "{\n  \"watch\": [\n    \"src\"\n  ],\n  \"ext\": \"ts\",\n  \"ignore\": [],\n  \"exec\": \"tsc && homebridge -I -D\",\n  \"signal\": \"SIGTERM\","
  },
  {
    "path": "package.json",
    "chars": 1647,
    "preview": "{\n  \"name\": \"homebridge-myq\",\n  \"version\": \"3.4.4\",\n  \"displayName\": \"Homebridge myQ\",\n  \"description\": \"HomeKit integra"
  },
  {
    "path": "src/index.ts",
    "chars": 429,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * index.ts: homebridge-myq plugin r"
  },
  {
    "path": "src/myq-device.ts",
    "chars": 5884,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-device.ts: Base class for all"
  },
  {
    "path": "src/myq-garagedoor.ts",
    "chars": 28217,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-garagedoor.ts: Garage door de"
  },
  {
    "path": "src/myq-lamp.ts",
    "chars": 7645,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-lamp.ts: Lamp device class fo"
  },
  {
    "path": "src/myq-mqtt.ts",
    "chars": 5943,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-mqtt.ts: MQTT connectivity cl"
  },
  {
    "path": "src/myq-options.ts",
    "chars": 7618,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-options.ts: Feature option an"
  },
  {
    "path": "src/myq-platform.ts",
    "chars": 14802,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * myq-platform.ts: homebridge-myq p"
  },
  {
    "path": "src/settings.ts",
    "chars": 1420,
    "preview": "/* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.\n *\n * settings.ts: Settings and constan"
  },
  {
    "path": "tsconfig.json",
    "chars": 416,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": true,\n    \"declaration\": true,\n    \"esModuleInterop\": true,"
  }
]

About this extraction

This page contains the full source code of the hjdhjd/homebridge-myq GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (191.8 KB), approximately 50.0k tokens, and a symbol index with 96 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!